Greasy Fork is available in English.

Steam Bundle Sites Extension

A steam bundle sites' tool kits.

Fra 06.10.2018. Se den seneste versjonen.

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

// ==UserScript==
// @name         Steam Bundle Sites Extension
// @homepage     https://github.com/clancy-chao/Steam-Bundle-Sites-Extension
// @namespace    http://tampermonkey.net/
// @version      2.7.1
// @description  A steam bundle sites' tool kits.
// @icon         http://store.steampowered.com/favicon.ico
// @author       Bisumaruko, Cloud
// @include      http*://store.steampowered.com/*
// @include      https://www.indiegala.com/gift*
// @include      https://www.indiegala.com/profile*
// @include      https://www.indiegala.com/game*
// @include      https://www.fanatical.com/*
// @include      https://www.humblebundle.com/*
// @include      http*://*dailyindiegame.com/*
// @include      http*://bundle.ccyycn.com/order/*
// @include      https://groupees.com/purchases
// @include      https://groupees.com/profile/purchases/*
// @include      http*://*agiso.com/*
// @include      https://steamdb.steamcn.com/tooltip*
// @include      https://yuplay.ru/orders/*/
// @include      https://yuplay.ru/product/*/
// @include      http*://gama-gama.ru/personal/settings/*
// @include      http*://plati.ru/seller/*
// @include      http*://plati.market/seller/*
// @include      http*://po.plati.ru/seller/*
// @include      http*://po.plati.market/seller/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.18.0/sweetalert2.min.js
// @resource     sweetalert2CSS https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.18.0/sweetalert2.min.css
// @resource     currencyFlags https://cdnjs.cloudflare.com/ajax/libs/currency-flags/1.5.0/currency-flags.min.css
// @resource     flagIcon https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/css/flag-icon.min.css
// @connect      store.steampowered.com
// @connect      www.google.com
// @connect      www.google.com.tw
// @connect      www.google.com.au
// @connect      www.google.co.jp
// @connect      www.google.co.nz
// @connect      www.google.co.uk
// @connect      www.google.ca
// @connect      www.google.de
// @connect      www.google.it
// @connect      www.google.fr
// @connect      www.ecb.europa.eu
// @connect      steamdb.steamcn.com
// @connect      steamdb.info
// @connect      steamspy.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

/* global swal */

// setup jQuery
const $ = jQuery.noConflict(true);

$.fn.pop = [].pop;
$.fn.shift = [].shift;

// inject external css styles
GM_addStyle(GM_getResourceText('sweetalert2CSS'));
GM_addStyle(GM_getResourceText('currencyFlags'));
GM_addStyle(GM_getResourceText('flagIcon').replace(/\.\.\//g, 'https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/'));

// inject script css styles
GM_addStyle(`
    pre.SBSE_errorMsg { height: 200px; text-align: left; white-space: pre-wrap; }
    a.SBSE_steamStore { text-decoration: none; font-size: smaller; }
    a.SBSE_steamStore:hover { text-decoration: none; }

    /* switch */
    .SBSE_switch { position: relative; display: inline-block; width: 60px; height: 30px; }
    .SBSE_switch input { display: none; }
    .SBSE_slider {
        position: absolute;
        top: 0; right: 0; bottom: 0; left: 0;
        background-color: #CCC;
        transition: 0.4s;
        cursor: pointer;
    }
    .SBSE_slider:before {
        width: 26px; height: 26px;
        position: absolute;
        bottom: 2px; left: 2px;
        background-color: white;
        transition: 0.4s;
        content: "";
    }
    .SBSE_switch input:checked + .SBSE_slider { background-color: #2196F3; }
    .SBSE_switch input:focus + .SBSE_slider { box-shadow: 0 0 1px #2196F3; }
    .SBSE_switch input:checked + .SBSE_slider:before { transform: translateX(30px); }
    .SBSE_switch-small { width: 40px; height: 20px; }
    .SBSE_switch-small .SBSE_slider:before { width: 16px; height: 16px; }
    .SBSE_switch-small input:checked + .SBSE_slider:before { transform: translateX(20px); }

    /* dropdown */
    .SBSE_dropdown { display: inline-block; position: relative; }
    .SBSE_dropdown_list {
        width: calc(100% - 10px);
        max-height: 0;
        display: inline-block;
        position: absolute;
        top: 35px; left: 0;
        padding: 0;
        transition: max-height 0.5s ease;
        overflow: hidden;
        list-style-type: none;
        background-color: #EEE;
    }
    .SBSE_dropdown_list > li { width: 100%; display: block; padding: 3px 0; text-align: center; }
    .SBSE_dropdown:hover > .SBSE_dropdown_list { max-height: 500px; }

    /* settings */
    .SBSE_settings .name { text-align: right; vertical-align: top; }
    .SBSE_settings .value { text-align: left; }
    .SBSE_settings .value > * { height: 30px; margin: 0 20px 10px; }
    .SBSE_settings > span { display: inline-block; color: white; cursor: pointer; }

    /* container */
    .SBSE_container {
        width: 100%; height: 200px;
        display: flex;
        flex-direction: column;
        box-sizing: border-box;
    }
    .SBSE_container > textarea {
        width: 100%; height: 150px;
        padding: 5px;
        border: none;
        box-sizing: border-box;
        resize: none;
        outline: none;
    }
    .SBSE_container > div { width: 100%; padding-top: 5px; box-sizing: border-box; }
    .SBSE_container > div button {
        width: 120px;
        position: relative;
        margin-right: 10px;
        line-height: 28px;
        transition: all 0.5s;
        box-sizing: border-box;
        outline: none;
        cursor: pointer;
    }
    .SBSE_container select { max-width:120px; height: 30px; }
    .SBSE_container label { margin-right: 10px; }
    .SBSE_ExportList a { text-decoration: none; color: #333; transition: color 0.3s ease; }
    .SBSE_ExportList a:hover { text-decoration: none; color: #787878; }
    #SBSE_BtnSettings {
        width: 20px; height: 20px;
        float: right;
        margin-top: 3px; margin-right: 0; margin-left: 10px;
        background-color: transparent;
        background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGhlaWdodD0iMzJweCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzIgMzI7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMycHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxnIGlkPSJMYXllcl8xIi8+PGcgaWQ9ImNvZyI+PHBhdGggZD0iTTMyLDE3Ljk2OXYtNGwtNC43ODEtMS45OTJjLTAuMTMzLTAuMzc1LTAuMjczLTAuNzM4LTAuNDQ1LTEuMDk0bDEuOTMtNC44MDVMMjUuODc1LDMuMjUgICBsLTQuNzYyLDEuOTYxYy0wLjM2My0wLjE3Ni0wLjczNC0wLjMyNC0xLjExNy0wLjQ2MUwxNy45NjksMGgtNGwtMS45NzcsNC43MzRjLTAuMzk4LDAuMTQxLTAuNzgxLDAuMjg5LTEuMTYsMC40NjlsLTQuNzU0LTEuOTEgICBMMy4yNSw2LjEyMWwxLjkzOCw0LjcxMUM1LDExLjIxOSw0Ljg0OCwxMS42MTMsNC43MDMsMTIuMDJMMCwxNC4wMzF2NGw0LjcwNywxLjk2MWMwLjE0NSwwLjQwNiwwLjMwMSwwLjgwMSwwLjQ4OCwxLjE4OCAgIGwtMS45MDIsNC43NDJsMi44MjgsMi44MjhsNC43MjMtMS45NDVjMC4zNzksMC4xOCwwLjc2NiwwLjMyNCwxLjE2NCwwLjQ2MUwxNC4wMzEsMzJoNGwxLjk4LTQuNzU4ICAgYzAuMzc5LTAuMTQxLDAuNzU0LTAuMjg5LDEuMTEzLTAuNDYxbDQuNzk3LDEuOTIybDIuODI4LTIuODI4bC0xLjk2OS00Ljc3M2MwLjE2OC0wLjM1OSwwLjMwNS0wLjcyMywwLjQzOC0xLjA5NEwzMiwxNy45Njl6ICAgIE0xNS45NjksMjJjLTMuMzEyLDAtNi0yLjY4OC02LTZzMi42ODgtNiw2LTZzNiwyLjY4OCw2LDZTMTkuMjgxLDIyLDE1Ljk2OSwyMnoiIHN0eWxlPSJmaWxsOiM0RTRFNTA7Ii8+PC9nPjwvc3ZnPg==);
        background-size: contain;
        background-repeat: no-repeat;
        background-origin: border-box;
        border: none;
        vertical-align: top;
    }

    /* spinner button affect */
    .SBSE_container > div > button:before {
        width: 20px; height: 20px;
        content: '';
        position: absolute;
        margin-top: 5px;
        right: 10px;
        border: 3px solid;
        border-left-color: transparent;
        border-radius: 50%;
        box-sizing: border-box;
        opacity: 0;
        transition: opacity 0.5s;
        animation-duration: 1s;
        animation-iteration-count: infinite;
        animation-name: rotate;
        animation-timing-function: linear;
    }
    .SBSE_container > div > button.narrow.working {
        width: 100px;
        padding-right: 40px;
        transition: all 0.5s;
    }
    .SBSE_container > div > button.working:before {
        transition-delay: 0.5s;
        transition-duration: 1s;
        opacity: 1;
    }
    @keyframes rotate {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
    }

    /* types */
    .SBSE_type {
        height: 20px;
        display: none;
        margin-right: 5px;
        justify-content: center;
    }
    .SBSE_type:before, .SBSE_type:after {
        content: '';
        box-sizing: border-box;
        pointer-events: none;
    }
    .SBSE_type:after { padding: 0 2px; }
    .SBSE_game .SBSE_type { background-color: rgba(97,100,101,0.3); }
    .SBSE_game .SBSE_type:after { content: 'Game'; }
    .SBSE_DLC .SBSE_type { background-color: rgba(165,84,177,0.8); }
    .SBSE_DLC .SBSE_type:before {
        content: 'ꜜ';
        width: 14px; height: 14px;
        margin: 3px 0 0 2px;
        border-radius: 50%;
        background-color: #000;
        color: rgba(165,84,177,0.8);
        text-align: center;
        font-size: 28px;
        line-height: 28px;
    }
    .SBSE_DLC .SBSE_type:after { content: 'DLC'; }
    .SBSE_package .SBSE_type { background-color: rgba(47,137,188,0.8); }
    .SBSE_package .SBSE_type:after { content: 'Package'; }
    .isSteam .SBSE_type, .showType .SBSE_type { display: flex; }
    .SBSE_type-vMiddle .SBSE_type { vertical-align: middle; }

    /* icons */
    .SBSE_icon {
        width: 20px; height: 20px;
        display: none;
        margin-left: 5px;
        border-radius: 50%;
        background-color: #E87A90;
        transform: rotate(45deg);
    }
    .SBSE_icon:before, .SBSE_icon:after {
        content: '';
        width: 3px; height: 14px;
        position: absolute;
        top: 50%; left: 50%;
        transform: translate(-50%, -50%);
        background-color: white;
        border-radius: 5px;
        pointer-events: none;
    }
    .SBSE_icon:after { transform: translate(-50%, -50%) rotate(-90deg); }
    .SBSE_owned .SBSE_icon { background-color: #9CCC65; }
    .SBSE_owned .SBSE_icon:before, .SBSE_owned .SBSE_icon:after { transform: none; }
    .SBSE_owned .SBSE_icon:before {
        width: 3px; height: 11px;
        top: 4px; left: 10px;
        border-radius: 5px 5px 5px 0;
    }
    .SBSE_owned .SBSE_icon:after {
        width: 5px; height: 3px;
        top: 12px; left: 6px;
        border-radius: 5px 0 0 5px;
    }
    .SBSE_wished .SBSE_icon { transform: rotate(0); background-color: #29B6F6; }
    .SBSE_wished .SBSE_icon:before, .SBSE_wished .SBSE_icon:after {
        width: 6px; height: 10px;
        top: 5px; left: 10px;
        border-radius: 6px 6px 0 0;
        transform: rotate(-45deg);
        transform-origin: 0 100%;
    }
    .SBSE_wished .SBSE_icon:after {
        left: 4px;
        transform: rotate(45deg);
        transform-origin :100% 100%;
    }
    .SBSE_ignored .SBSE_icon { background-color: rgb(135, 173, 189); }
    .SBSE_notApplicable .SBSE_icon { transform: rotate(0); background-color: rgb(248, 187, 134); }
    .SBSE_notApplicable .SBSE_icon:before {
        content: '?';
        width: 0; height: 10px;
        top: 5px; left: 7px;
        color: white;
        font-size: 16px; font-weight: 900;
    }
    .SBSE_notApplicable .SBSE_icon:after { display: none; }
    .SBSE_fetching .SBSE_icon { transform: rotate(0); background-color: transparent; }
    .SBSE_fetching .SBSE_icon:before {
        width: 20px; height: 20px;
        top: 0; left: 0;
        border: 3px solid grey;
        border-left-color: transparent;
        border-radius: 50%;
        box-sizing: border-box;
        transition: opacity 0.5s;
        animation-duration: 1s;
        animation-iteration-count: infinite;
        animation-name: rotate;
        animation-timing-function: linear;
    }
    .SBSE_fetching .SBSE_icon:after { display: none; }
    .SBSE_notFetched .SBSE_icon { background-color: transparent; }
    .SBSE_notFetched .SBSE_icon:before, .SBSE_notFetched .SBSE_icon:after { display: none; }
    .SBSE_failed .SBSE_icon { transform: rotate(0); }
    .SBSE_failed .SBSE_icon:before {
        content: '!';
        width: 0; height: 10px;
        top: 5px; left: 8.5px;
        color: white;
        font-size: 16px; font-weight: 900;
    }
    .SBSE_failed .SBSE_icon:after { display: none; }
    .isSteam .SBSE_icon, .showIcon .SBSE_icon { display: inline-block; }
    .SBSE_icon-vMiddle .SBSE_icon { vertical-align: middle; }

    /* Steam Tooltip */
    .SBSE_tooltip {
        width: 308px;
        position: fixed;
        overflow: hidden;
        background: url(https://steamstore-a.akamaihd.net/public/images/v6/blue_body_darker_repeat.jpg) -700px center repeat-y scroll rgb(0, 0, 0);
        border: 0;
        box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
        transition: all 0.5s;
        z-index: 999;
    }
    .SBSE_tooltip.SBSE_hide { display: none; }

    /* Tooltip */
    [tooltip]::before, [tooltip]::after {
        position: absolute;
        opacity: 0;
        transition: all 0.15s ease;
    }
    [tooltip]::before {
        width: max-content;
        content: attr(tooltip);
        top: calc(100% + 10px); left: 0;
        padding: 10px;
        color: #4a4c45;
        background-color: white;
        border-radius: 3px;
        box-shadow: 1px 2px 3px rgba(0,0,0,0.45);
    }
    [tooltip]::after {
        content: "";
        top: calc(100% + 5px); left: 10px;
        border-left: 5px solid transparent;
        border-right: 5px solid transparent;
        border-bottom: 5px solid white;
    }
    [tooltip]:hover::before, [tooltip]:hover::after { opacity: 1; }
    [tooltip]:not([tooltip-persistent])::before, [tooltip]:not([tooltip-persistent])::after { pointer-events: none; }
`);

// load up
const regURL = /(https?:\/\/)?([.\w]*steam[-.\w]*){1}\/.*?(apps?|subs?){1}\/(\d+){1}(\/.*\/?)?/m;
const regKey = /(?:(?:([A-Z0-9])(?!\1{4})){5}-){2,5}[A-Z0-9]{5}/g;
const eol = "\n";
const tab = "\t";
const has = Object.prototype.hasOwnProperty;
const unique = a => [...new Set(a)];
const isArray = value => Array.isArray(value);
const isObject = value => Object(value) === value;

const config = {
    data: JSON.parse(GM_getValue('SBSE_config', '{}')),
    set(key, value, callback) {
        this.data[key] = value;
        GM_setValue('SBSE_config', JSON.stringify(this.data));

        if (typeof callback === 'function') callback();
    },
    get(key) {
        return has.call(this.data, key) ? this.data[key] : null;
    },
    init() {
        if (!has.call(this.data, 'autoUpdateSessionID')) this.data.autoUpdateSessionID = true;
        if (!has.call(this.data, 'ASFFormat')) this.data.ASFFormat = false;
        if (!has.call(this.data, 'titleComesLast')) this.data.titleComesLast = false;
        if (!has.call(this.data, 'activateAllKeys')) this.data.activateAllKeys = false;
        if (!has.call(this.data, 'enableTooltips')) this.data.enableTooltips = this.get('language') !== 'english';
    }
};
const i18n = {
    data: {
        tchinese: {
            name: '繁體中文',
            updateSuccessTitle: '更新成功!',
            updateSuccess: '成功更新Steam sessionID',
            successStatus: '成功',
            successTitle: '好極了!',
            successDetail: '無資料',
            skippedStatus: '跳過',
            activatedDetail: '已啟動',
            failStatus: '失敗',
            failTitle: '糟糕!',
            failDetailUnexpected: '發生未知錯誤,請稍後再試',
            failDetailInvalidKey: '序號錯誤',
            failDetailUsedKey: '序號已被使用',
            failDetailRateLimited: '啟動受限',
            failDetailCountryRestricted: '地區限制',
            failDetailAlreadyOwned: '產品已擁有',
            failDetailMissingBaseGame: '未擁有主程式',
            failDetailPS3Required: '需要PS3 啟動',
            failDetailGiftWallet: '偵測到禮物卡/錢包序號',
            failDetailParsingFailed: '處理資料發生錯誤,請稍後再試',
            failDetailRequestFailedNeedUpdate: '請求發生錯誤,請稍後再試<br/>或者嘗試更新SessionID',
            noItemDetails: '無產品詳細資料',
            notLoggedInTitle: '未登入',
            notLoggedInMsg: '請登入Steam 以讓腳本紀錄SessionID',
            missingTitle: '未發現SessionID',
            missingMsg: '請問要更新SessionID 嗎?',
            emptyInput: '未發現Steam 序號',
            settingsTitle: '設定',
            settingsAutoUpdateSessionID: '自動更新SessionID',
            settingsSessionID: '我的SessionID',
            settingsSyncLibrary: '同步遊戲庫資料',
            settingsSyncLibraryButton: '同步',
            settingsLanguage: '語言',
            settingsASFFormat: '啟用ASF 格式',
            settingsTitleComesLast: '遊戲名置後',
            settingsActivateAllKeys: '不跳過、啟動所有序號',
            settingsEnableTooltips: 'SteamCN 論壇提示框',
            HBAlreadyOwned: '遊戲已擁有',
            HBRedeemAlreadyOwned: '確定刮開 %title% Steam 序號?',
            steamStore: 'Steam 商店',
            HBActivationRestrictions: '啟動限制',
            HBDisallowedCountries: '限制以下地區啟動',
            HBExclusiveCountries: '僅限以下地區啟動',
            HBCurrentLocation: '當前位於:',
            DIGEasyBuyPurchase: '購買',
            DIGEasyBuySelectAll: '全選',
            DIGEasyBuySelectCancel: '取消',
            DIGEasyBuyHideOwned: '隱藏已擁有',
            DIGEasyBuyShowOwned: '顯示已擁有',
            DIGEasyBuyLoadAllPages: '加載所有頁',
            DIGEasyBuyLoading: '加載第%page%頁中',
            DIGEasyBuyLoadingComplete: '加載完成',
            DIGButtonPurchasing: '購買中',
            DIGInsufficientFund: '餘額不足',
            DIGFinishedPurchasing: '購買完成',
            DIGMarketSearchResult: '目前市集上架中',
            DIGRateAllPositive: '全部好評',
            DIGClickToHideThisRow: '隱藏此上架遊戲',
            buttonReveal: '刮開',
            buttonRetrieve: '提取',
            buttonActivate: '啟動',
            buttonCopy: '複製',
            buttonReset: '清空',
            buttonExport: '匯出',
            checkboxIncludeGameTitle: '遊戲名',
            checkboxJoinKeys: '合併',
            checkboxSkipUsed: '跳過已使用',
            checkboxMarketListings: '上架於市集',
            selectFilterAll: '選取全部',
            selectFilterOwned: '選取已擁有',
            selectFilterNotOwned: '選取未擁有',
            selectConnector: '至',
            markAllAsUsed: '標記全部已使用',
            syncSuccessTitle: '同步成功',
            syncSuccess: '成功同步Steam 遊戲庫資料',
            syncFailTitle: '同步失敗',
            syncFail: '失敗同步Steam 遊戲庫資料',
            visitSteam: '前往Steam',
            lastSyncTime: '已於%seconds% 秒前同步收藏庫',
            game: '遊戲',
            dlc: 'DLC',
            package: '合集',
            bundle: '組合包',
            owned: '已擁有',
            wished: '於願望清單',
            ignored: '已忽略',
            notOwned: '未擁有',
            notApplicable: '無資料',
            notFetched: '未檢查',
            enablePlatiFeature: '啟用腳本',
            platiFetchOnStart: '自動檢查',
            platiFetchButton: '檢查',
            platiFilterType: '顯示類型',
            platiFilterStatus: '顯示狀態'
        },
        schinese: {
            name: '简体中文',
            updateSuccessTitle: '更新成功',
            updateSuccess: '成功更新Steam sessionID',
            successStatus: '成功',
            successTitle: '好极了!',
            successDetail: '无信息',
            activatedDetail: '已激活',
            skippedStatus: '跳过',
            failStatus: '失败',
            failTitle: '糟糕!',
            failDetailUnexpected: '发生未知错误,请稍后再试',
            failDetailInvalidKey: '激活码错误',
            failDetailUsedKey: '激活码已被使用',
            failDetailRateLimited: '激活受限',
            failDetailCountryRestricted: '地区限制',
            failDetailAlreadyOwned: '产品已拥有',
            failDetailMissingBaseGame: '未拥有基础游戏',
            failDetailPS3Required: '需要PS3 激活',
            failDetailGiftWallet: '侦测到礼物卡/钱包激活码',
            failDetailParsingFailed: '处理资料发生错误,请稍后再试',
            failDetailRequestFailedNeedUpdate: '请求发生错误,请稍后再试<br/>或者尝试更新SessionID',
            noItemDetails: '无产品详细信息',
            notLoggedInTitle: '未登入',
            notLoggedInMsg: '请登入Steam 以让脚本记录SessionID',
            missingTitle: '未发现SessionID',
            missingMsg: '请问要更新SessionID 吗?',
            emptyInput: '未批配到Steam 激活码',
            settingsTitle: '设置',
            settingsAutoUpdateSessionID: '自动更新SessionID',
            settingsSessionID: '我的SessionID',
            settingsSyncLibrary: '同步游戏库资料',
            settingsSyncLibraryButton: '同步',
            settingsLanguage: '语言',
            settingsASFFormat: '启用ASF 格式',
            settingsTitleComesLast: '游戏名置后',
            settingsActivateAllKeys: '不跳过、激活所有激活码',
            settingsEnableTooltips: 'SteamCN 论坛提示窗',
            HBAlreadyOwned: '游戏已拥有',
            HBRedeemAlreadyOwned: '确定刮开 %title% Steam 激活码?',
            steamStore: 'Steam 商店',
            HBActivationRestrictions: '激活限制',
            HBDisallowedCountries: '限制以下地区激活',
            HBExclusiveCountries: '仅限以下地区激活',
            HBCurrentLocation: '当前位于:',
            DIGEasyBuyPurchase: '购买',
            DIGEasyBuySelectAll: '全选',
            DIGEasyBuySelectCancel: '取消',
            DIGEasyBuyHideOwned: '隐藏已拥有',
            DIGEasyBuyShowOwned: '显示已拥有',
            DIGEasyBuyLoadAllPages: '加载所有页',
            DIGEasyBuyLoading: '加载第%page%页中',
            DIGEasyBuyLoadingComplete: '加载完成',
            DIGButtonPurchasing: '购买中',
            DIGInsufficientFund: '余额不足',
            DIGFinishedPurchasing: '购买完成',
            DIGMarketSearchResult: '目前市集上架中',
            DIGRateAllPositive: '全部好评',
            DIGClickToHideThisRow: '隐藏此上架游戏',
            buttonReveal: '刮开',
            buttonRetrieve: '提取',
            buttonActivate: '激活',
            buttonCopy: '复制',
            buttonReset: '清空',
            buttonExport: '导出',
            checkboxIncludeGameTitle: '游戏名',
            checkboxJoinKeys: '合并',
            checkboxSkipUsed: '跳过已使用',
            checkboxMarketListings: '上架于市集',
            selectFilterAll: '选取全部',
            selectFilterOwned: '选取已拥有',
            selectFilterNotOwned: '选取未拥有',
            selectConnector: '至',
            markAllAsUsed: '标记全部已使用',
            syncSuccessTitle: '同步成功',
            syncSuccess: '成功同步Steam 游戏库资料',
            syncFailTitle: '同步失败',
            syncFail: '失败同步Steam 游戏库资料',
            visitSteam: '前往Steam',
            lastSyncTime: '已于%seconds% 秒前同步游戏库',
            game: '游戏',
            dlc: 'DLC',
            package: '礼包',
            bundle: '捆绑包',
            owned: '已拥有',
            wished: '于愿望清单',
            ignored: '已忽略',
            notOwned: '未拥有',
            notApplicable: '无资料',
            notFetched: '未检查',
            enablePlatiFeature: '启用脚本',
            platiFetchOnStart: '自动检查',
            platiFetchButton: '检查',
            platiFilterType: '显示类型',
            platiFilterStatus: '显示状态'
        },
        english: {
            name: 'English',
            updateSuccessTitle: 'Update Successful!',
            updateSuccess: 'Steam sessionID is successfully updated',
            successStatus: 'Success',
            successTitle: 'Hurray!',
            successDetail: 'No Detail',
            activatedDetail: 'Activated',
            skippedStatus: 'Skipped',
            failStatus: 'Fail',
            failTitle: 'Opps!',
            failDetailUnexpected: 'Unexpected Error',
            failDetailInvalidKey: 'Invalid Key',
            failDetailUsedKey: 'Used Key',
            failDetailRateLimited: 'Rate Limited',
            failDetailCountryRestricted: 'Country Restricted',
            failDetailAlreadyOwned: 'Product Already Owned',
            failDetailMissingBaseGame: 'Missing Base Game',
            failDetailPS3Required: 'PS3 Activation Required',
            failDetailGiftWallet: 'Gift Card/Wallet Code Detected',
            failDetailParsingFailed: 'Result parse failed',
            failDetailRequestFailedNeedUpdate: 'Request failed, please try again<br/>or update sessionID',
            noItemDetails: 'No Item Details',
            notLoggedInTitle: 'Not Logged-In',
            notLoggedInMsg: 'Please login to Steam so sessionID can be saved',
            missingTitle: 'Missing SessionID',
            missingMsg: 'Do you want to update your Steam sessionID?',
            emptyInput: 'Could not find Steam code',
            settingsTitle: 'Settings',
            settingsAutoUpdateSessionID: 'Auto Update SessionID',
            settingsSessionID: 'Your sessionID',
            settingsSyncLibrary: 'Sync Library Data',
            settingsSyncLibraryButton: 'Sync',
            settingsLanguage: 'Language',
            settingsASFFormat: 'Enable ASF Format',
            settingsTitleComesLast: 'Title Comes Last',
            settingsActivateAllKeys: 'No skip & activate all keys',
            settingsEnableTooltips: 'Tooltips from SteamCN',
            HBAlreadyOwned: 'Game Already Owned',
            HBRedeemAlreadyOwned: 'Are you sure to redeem %title% Steam Key?',
            steamStore: 'Steam Store',
            HBActivationRestrictions: 'Activation Restrictions',
            HBDisallowedCountries: 'Cannot be activated in the following regions',
            HBExclusiveCountries: 'Can only be activated in the following regions',
            HBCurrentLocation: 'Current Location: ',
            DIGEasyBuyPurchase: 'Purchase',
            DIGEasyBuySelectAll: 'Select All',
            DIGEasyBuySelectCancel: 'Cancel',
            DIGEasyBuyHideOwned: 'Hide Owned',
            DIGEasyBuyShowOwned: 'Show Owned',
            DIGEasyBuyLoadAllPages: 'Load All Pages',
            DIGEasyBuyLoading: 'Loading page %page%',
            DIGEasyBuyLoadingComplete: 'Loaded',
            DIGButtonPurchasing: 'Purchassing',
            DIGInsufficientFund: 'Insufficient fund',
            DIGFinishedPurchasing: 'Finished Purchasing',
            DIGMarketSearchResult: 'Currently listing in marketplace',
            DIGRateAllPositive: 'Mark All Positive',
            DIGClickToHideThisRow: 'Hide this game from listings',
            buttonReveal: 'Reveal',
            buttonRetrieve: 'Retrieve',
            buttonActivate: 'Activate',
            buttonCopy: 'Copy',
            buttonReset: 'Reset',
            buttonExport: 'Export',
            checkboxIncludeGameTitle: 'Game Title',
            checkboxJoinKeys: 'Join',
            checkboxSkipUsed: 'Skip Used',
            checkboxMarketListings: 'Market Listings',
            selectFilterAll: 'Select All',
            selectFilterOwned: 'Select Owned',
            selectFilterNotOwned: 'Select Not Owned',
            selectConnector: 'to',
            markAllAsUsed: 'Mark All as Used',
            syncSuccessTitle: 'Sync Successful',
            syncSuccess: 'Successfully sync Steam library data',
            syncFailTitle: 'Sync failed',
            syncFail: 'Failed to sync Steam library data',
            visitSteam: 'Visit Steam',
            lastSyncTime: 'Library data synced %seconds% seconds ago',
            game: 'Game',
            dlc: 'DLC',
            package: 'Package',
            bundle: 'Bundle',
            owned: 'Owned',
            wished: 'Wishlisted',
            ignored: 'Ignored',
            notOwned: 'Not Owned',
            notApplicable: 'Not Applicable',
            notFetched: 'Not Checked',
            enablePlatiFeature: 'Enable Script',
            platiFetchOnStart: 'Auto Check',
            platiFetchButton: 'Check',
            platiFilterType: 'Show Type',
            platiFilterStatus: 'Show Status'
        }
    },
    language: null,
    set() {
        const selectedLanguage = has.call(this.data, config.get('language')) ? config.get('language') : 'english';

        this.language = this.data[selectedLanguage];
    },
    get(key) {
        return has.call(this.language, key) ? this.language[key] : this.data.english[key];
    },
    init() {
        this.set();
    }
};
const ISO2 = {
    name: {
        tchinese: {
            AD: '安道爾',
            AE: '阿拉伯聯合大公國',
            AF: '阿富汗',
            AG: '安地卡及巴布達',
            AI: '安圭拉',
            AL: '阿爾巴尼亞',
            AM: '亞美尼亞',
            AO: '安哥拉',
            AQ: '南極洲',
            AR: '阿根廷',
            AS: '美屬薩摩亞',
            AT: '奧地利',
            AU: '澳大利亞',
            AW: '阿魯巴',
            AX: '奧蘭',
            AZ: '亞塞拜然',
            BA: '波士尼亞與赫塞哥維納',
            BB: '巴貝多',
            BD: '孟加拉',
            BE: '比利時',
            BF: '布吉納法索',
            BG: '保加利亞',
            BH: '巴林',
            BI: '蒲隆地',
            BJ: '貝南',
            BL: '聖巴泰勒米',
            BM: '百慕達',
            BN: '汶萊',
            BO: '玻利維亞',
            BQ: '波奈',
            BR: '巴西',
            BS: '巴哈馬',
            BT: '不丹',
            BV: '布威島',
            BW: '波札那',
            BY: '白俄羅斯',
            BZ: '貝里斯',
            CA: '加拿大',
            CC: '科科斯(基林)群島',
            CD: '剛果民主共和國',
            CF: '中非共和國',
            CG: '剛果共和國',
            CH: '瑞士',
            CI: '象牙海岸',
            CK: '庫克群島',
            CL: '智利',
            CM: '喀麥隆',
            CN: '中國',
            CO: '哥倫比亞',
            CR: '哥斯大黎加',
            CS: '塞爾維亞與蒙特內哥羅',
            CU: '古巴',
            CV: '維德角',
            CW: '古拉索',
            CX: '聖誕島',
            CY: '賽普勒斯',
            CZ: '捷克',
            DE: '德國',
            DJ: '吉布地',
            DK: '丹麥',
            DM: '多米尼克',
            DO: '多明尼加',
            DZ: '阿爾及利亞',
            EC: '厄瓜多',
            EE: '愛沙尼亞',
            EG: '埃及',
            EH: '西撒哈拉',
            ER: '厄利垂亞',
            ES: '西班牙',
            ET: '衣索比亞',
            FI: '芬蘭',
            FJ: '斐濟',
            FK: '福克蘭群島',
            FM: '密克羅尼西亞聯邦',
            FO: '法羅群島',
            FR: '法國',
            GA: '加彭',
            GB: '英國',
            GD: '格瑞那達',
            GE: '喬治亞',
            GF: '法屬圭亞那',
            GG: '根西',
            GH: '迦納',
            GI: '直布羅陀',
            GL: '格陵蘭',
            GM: '甘比亞',
            GN: '幾內亞',
            GP: '瓜德羅普',
            GQ: '赤道幾內亞',
            GR: '希臘',
            GS: '南喬治亞與南桑威奇',
            GT: '瓜地馬拉',
            GU: '關島',
            GW: '幾內亞比索',
            GY: '蓋亞那',
            HK: '香港',
            HM: '赫德島和麥克唐納群島',
            HN: '宏都拉斯',
            HR: '克羅埃西亞',
            HT: '海地',
            HU: '匈牙利',
            ID: '印尼',
            IE: '愛爾蘭',
            IL: '以色列',
            IM: '曼島',
            IN: '印度',
            IO: '英屬印度洋領地',
            IQ: '伊拉克',
            IR: '伊朗',
            IS: '冰島',
            IT: '義大利',
            JE: '澤西',
            JM: '牙買加',
            JO: '約旦',
            JP: '日本',
            KE: '肯亞',
            KG: '吉爾吉斯',
            KH: '柬埔寨',
            KI: '吉里巴斯',
            KM: '葛摩',
            KN: '聖克里斯多福及尼維斯',
            KP: '北韓',
            KR: '南韓',
            KW: '科威特',
            KY: '開曼群島',
            KZ: '哈薩克',
            LA: '寮國',
            LB: '黎巴嫩',
            LC: '聖露西亞',
            LI: '列支敦斯登',
            LK: '斯里蘭卡',
            LR: '賴比瑞亞',
            LS: '賴索托',
            LT: '立陶宛',
            LU: '盧森堡',
            LV: '拉脫維亞',
            LY: '利比亞',
            MA: '摩洛哥',
            MC: '摩納哥',
            MD: '摩爾多瓦',
            ME: '蒙特內哥羅',
            MF: '法屬聖馬丁',
            MG: '馬達加斯加',
            MH: '馬紹爾群島',
            MK: '馬其頓共和國',
            ML: '馬利',
            MM: '緬甸',
            MN: '蒙古',
            MO: '澳門',
            MP: '北馬里亞納群島',
            MQ: '馬丁尼克',
            MR: '茅利塔尼亞',
            MS: '蒙哲臘',
            MT: '馬爾他',
            MU: '模里西斯',
            MV: '馬爾地夫',
            MW: '馬拉威',
            MX: '墨西哥',
            MY: '馬來西亞',
            MZ: '莫三比克',
            NA: '納米比亞',
            NC: '新喀里多尼亞',
            NE: '尼日',
            NF: '諾福克島',
            NG: '奈及利亞',
            NI: '尼加拉瓜',
            NL: '荷蘭',
            NO: '挪威',
            NP: '尼泊爾',
            NR: '諾魯',
            NU: '紐埃',
            NZ: '紐西蘭',
            OM: '阿曼',
            PA: '巴拿馬',
            PE: '秘魯',
            PF: '法屬玻里尼西亞',
            PG: '巴布亞紐幾內亞',
            PH: '菲律賓',
            PK: '巴基斯坦',
            PL: '波瀾',
            PM: '聖皮耶與密克隆群島',
            PN: '皮特肯群島',
            PR: '波多黎各',
            PS: '巴勒斯坦',
            PT: '葡萄牙',
            PW: '帛琉',
            PY: '巴拉圭',
            QA: '卡達',
            RE: '留尼旺',
            RO: '羅馬尼亞',
            RS: '塞爾維亞',
            RU: '俄羅斯',
            RW: '盧安達',
            SA: '沙烏地阿拉伯',
            SB: '索羅門群島',
            SC: '塞席爾',
            SD: '蘇丹',
            SE: '瑞典',
            SG: '新加坡',
            SH: '聖赫勒拿、亞森欣與垂斯坦昆哈',
            SI: '斯洛維尼亞',
            SJ: '斯瓦巴和揚馬延',
            SK: '斯洛伐克',
            SL: '獅子山共和國',
            SM: '聖馬利諾',
            SN: '塞內加爾',
            SO: '索馬利亞',
            SR: '蘇利南',
            SS: '南蘇丹',
            ST: '聖多美普林西比',
            SV: '薩爾瓦多',
            SX: '荷屬聖馬丁',
            SY: '敘利亞',
            SZ: '史瓦濟蘭',
            TC: '土克凱可群島',
            TD: '查德',
            TF: '法屬南部和南極領地',
            TG: '多哥',
            TH: '泰國',
            TJ: '塔吉克',
            TK: '托克勞',
            TL: '東帝汶',
            TM: '土庫曼',
            TN: '突尼西亞',
            TO: '東加',
            TR: '土耳其',
            TT: '千里達及托巴哥',
            TV: '吐瓦魯',
            TW: '臺灣',
            TZ: '坦尚尼亞',
            UA: '烏克蘭',
            UG: '烏干達',
            UM: '美國本土外小島嶼',
            US: '美國',
            UY: '烏拉圭',
            UZ: '烏茲別克',
            VA: '聖座',
            VC: '聖文森及格瑞那丁',
            VE: '委內瑞拉',
            VG: '英屬維京群島',
            VI: '美屬維京群島',
            VN: '越南',
            VU: '萬那杜',
            WF: '瓦利斯和富圖納',
            WS: '薩摩亞',
            XK: '科索沃',
            YE: '葉門',
            YT: '馬約特',
            ZA: '南非',
            ZM: '尚比亞',
            ZW: '辛巴威'
        },
        schinese: {
            AD: '安道尔',
            AE: '阿拉伯联合酋长国',
            AF: '阿富汗',
            AG: '安提瓜和巴布达',
            AI: '安圭拉',
            AL: '阿尔巴尼亚',
            AM: '亚美尼亚',
            AO: '安哥拉',
            AQ: '南极洲',
            AR: '阿根廷',
            AS: '美属萨摩亚',
            AT: '奥地利',
            AU: '澳大利亚',
            AW: '阿鲁巴',
            AX: '奥兰群岛',
            AZ: '阿塞拜疆',
            BA: '波斯尼亚和黑塞哥维那',
            BB: '巴巴多斯',
            BD: '孟加拉',
            BE: '比利时',
            BF: '布基纳法索',
            BG: '保加利亚',
            BH: '巴林',
            BI: '布隆迪',
            BJ: '贝宁',
            BL: '圣巴托洛缪岛',
            BM: '百慕大',
            BN: '文莱',
            BO: '玻利维亚',
            BQ: '博奈尔',
            BR: '巴西',
            BS: '巴哈马',
            BT: '不丹',
            BV: '布韦岛',
            BW: '博兹瓦纳',
            BY: '白俄罗斯',
            BZ: '伯利兹',
            CA: '加拿大',
            CC: '科科斯(基林)群岛',
            CD: '刚果(金)',
            CF: '中非共和国',
            CG: '刚果(布)',
            CH: '瑞士',
            CI: '科特迪瓦',
            CK: '库克群岛',
            CL: '智利',
            CM: '喀麦隆',
            CN: '中国',
            CO: '哥伦比亚',
            CR: '哥斯达黎加',
            CS: '塞尔维亚和黑山',
            CU: '古巴',
            CV: '佛得角',
            CW: '库拉索',
            CX: '圣诞岛',
            CY: '塞浦路斯',
            CZ: '捷克',
            DE: '德国',
            DJ: '吉布提',
            DK: '丹麦',
            DM: '多米尼克',
            DO: '多米尼加',
            DZ: '阿尔及利亚',
            EC: '厄瓜多尔',
            EE: '爱沙尼亚',
            EG: '埃及',
            EH: '西撒哈拉',
            ER: '厄立特里亚',
            ES: '西班牙',
            ET: '埃塞俄比亚',
            FI: '芬兰',
            FJ: '斐济',
            FK: '福克兰群岛',
            FM: '密克罗尼西亚',
            FO: '法罗群岛',
            FR: '法国',
            GA: '加蓬',
            GB: '英国',
            GD: '格林纳达',
            GE: '格鲁吉亚',
            GF: '法属圭亚那',
            GG: '根西',
            GH: '加纳',
            GI: '直布罗陀',
            GL: '格陵兰',
            GM: '冈比亚',
            GN: '几内亚',
            GP: '瓜德鲁普',
            GQ: '赤道几内亚',
            GR: '希腊',
            GS: '南乔治亚岛和南桑威奇群岛',
            GT: '危地马拉',
            GU: '关岛',
            GW: '几内亚比绍',
            GY: '圭亚那',
            HK: '香港',
            HM: '赫德岛和麦克唐纳群岛',
            HN: '洪都拉斯',
            HR: '克罗地亚',
            HT: '海地',
            HU: '匈牙利',
            ID: '印尼',
            IE: '爱尔兰',
            IL: '以色列',
            IM: '马恩岛',
            IN: '印度',
            IO: '英属印度洋领地',
            IQ: '伊拉克',
            IR: '伊朗',
            IS: '冰岛',
            IT: '意大利',
            JE: '泽西岛',
            JM: '牙买加',
            JO: '约旦',
            JP: '日本',
            KE: '肯尼亚',
            KG: '吉尔吉斯',
            KH: '柬埔寨',
            KI: '基里巴斯',
            KM: '科摩罗',
            KN: '圣基茨和尼维斯',
            KP: '朝鲜',
            KR: '韩国',
            KW: '科威特',
            KY: '开曼群岛',
            KZ: '哈萨克斯坦',
            LA: '老挝',
            LB: '黎巴嫩',
            LC: '圣卢西亚',
            LI: '列支敦士登',
            LK: '斯里兰卡',
            LR: '利比里亚',
            LS: '莱索托',
            LT: '立陶宛',
            LU: '卢森堡',
            LV: '拉脱维亚',
            LY: '利比亚',
            MA: '摩洛哥',
            MC: '摩纳哥',
            MD: '摩尔多瓦',
            ME: '黑山',
            MF: '法属圣马丁',
            MG: '马达加斯加',
            MH: '马绍尔群岛',
            MK: '马其顿',
            ML: '马里',
            MM: '缅甸',
            MN: '蒙古',
            MO: '澳门',
            MP: '北马里亚纳群岛',
            MQ: '马提尼克',
            MR: '毛里塔尼亚',
            MS: '蒙塞拉特',
            MT: '马耳他',
            MU: '毛里求斯',
            MV: '马尔代夫',
            MW: '马拉维',
            MX: '墨西哥',
            MY: '马来西亚',
            MZ: '莫桑比克',
            NA: '纳米比亚',
            NC: '新喀里多尼亚',
            NE: '尼日尔',
            NF: '诺福克岛',
            NG: '尼日利',
            NI: '尼加拉瓜',
            NL: '荷兰',
            NO: '挪威',
            NP: '尼泊尔',
            NR: '瑙鲁',
            NU: '纽埃',
            NZ: '新西兰',
            OM: '阿曼',
            PA: '巴拿马',
            PE: '秘鲁',
            PF: '法属波利尼西亚a',
            PG: '巴布亚新几内亚',
            PH: '菲律宾',
            PK: '巴基斯坦',
            PL: '波兰',
            PM: '圣皮埃尔和密克隆',
            PN: '皮特凯恩群岛',
            PR: '波多黎各',
            PS: '巴勒斯坦',
            PT: '葡萄牙',
            PW: '帕劳',
            PY: '巴拉圭',
            QA: '卡塔尔',
            RE: '留尼旺島',
            RO: '罗马尼亚',
            RS: '塞尔维亚',
            RU: '俄罗斯',
            RW: '卢旺达',
            SA: '沙特阿拉伯',
            SB: '所罗门群岛',
            SC: '塞舌尔',
            SD: '苏丹',
            SE: '瑞典',
            SG: '新加坡',
            SH: '圣赫勒拿、阿森松与特斯坦达库尼亚',
            SI: '斯洛文尼',
            SJ: '斯瓦尔巴群岛和扬马延岛',
            SK: '斯洛伐克',
            SL: '塞拉利昂',
            SM: '圣马力诺',
            SN: '塞内加尔',
            SO: '索马里',
            SR: '苏里南',
            SS: '南苏丹',
            ST: '圣多美和普林西比',
            SV: '萨尔瓦多',
            SX: '荷属圣马丁',
            SY: '叙利亚',
            SZ: '斯威士兰',
            TC: '特克斯和凯科斯群岛',
            TD: '乍得',
            TF: '法属南部领土',
            TG: '多哥',
            TH: '泰国',
            TJ: '塔吉克斯坦',
            TK: '托克劳',
            TL: '东帝汶',
            TM: '土库曼斯坦',
            TN: '突尼斯',
            TO: '汤加',
            TR: '土耳其',
            TT: '特立尼达和多巴哥',
            TV: '图瓦卢',
            TW: '台湾',
            TZ: '坦桑尼亚',
            UA: '乌克兰',
            UG: '乌干达',
            UM: '美国本土外小岛屿',
            US: '美国',
            UY: '乌拉圭',
            UZ: '乌兹别克斯坦',
            VA: '圣座',
            VC: '圣文森特和格林纳丁斯',
            VE: '委内瑞拉',
            VG: '英属维尔京群岛',
            VI: '美属维尔京群岛',
            VN: '越南',
            VU: '瓦努阿图',
            WF: '瓦利斯和富图纳群岛',
            WS: '萨摩亚',
            XK: '科索沃',
            YE: '也门',
            YT: '马约特',
            ZA: '南非',
            ZM: '赞比亚',
            ZW: '津巴布韦'
        },
        english: {
            AD: 'Andorra',
            AE: 'United Arab Emirates',
            AF: 'Afghanistan',
            AG: 'Antigua and Barbuda',
            AI: 'Anguilla',
            AL: 'Albania',
            AM: 'Armenia',
            AO: 'Angola',
            AQ: 'Antarctica',
            AR: 'Argentina',
            AS: 'American Samoa',
            AT: 'Austria',
            AU: 'Australia',
            AW: 'Aruba',
            AX: 'Aland Islands',
            AZ: 'Azerbaijan',
            BA: 'Bosnia and Herzegovina',
            BB: 'Barbados',
            BD: 'Bangladesh',
            BE: 'Belgium',
            BF: 'Burkina Faso',
            BG: 'Bulgaria',
            BH: 'Bahrain',
            BI: 'Burundi',
            BJ: 'Benin',
            BL: 'Saint Barthélemy',
            BM: 'Bermuda',
            BN: 'Brunei',
            BO: 'Bolivia',
            BQ: 'Bonaire',
            BR: 'Brazil',
            BS: 'Bahamas',
            BT: 'Bhutan',
            BV: 'Bouvet Island',
            BW: 'Botswana',
            BY: 'Belarus',
            BZ: 'Belize',
            CA: 'Canada',
            CC: 'Cocos (Keeling) Islands',
            CD: 'East Congo',
            CF: 'Central African Republic',
            CG: 'West Congo',
            CH: 'Switzerland',
            CI: 'Ivory Coast',
            CK: 'Cook Islands',
            CL: 'Chile',
            CM: 'Cameroon',
            CN: 'China',
            CO: 'Colombia',
            CR: 'Costa Rica',
            CS: 'Serbia and Montenegro',
            CU: 'Cuba',
            CV: 'Cabo Verde',
            CW: 'Curaçao',
            CX: 'Christmas Island',
            CY: 'Cyprus',
            CZ: 'Czechia',
            DE: 'Germany',
            DJ: 'Djibouti',
            DK: 'Denmark',
            DM: 'Dominica',
            DO: 'Dominican Republic',
            DZ: 'Algeria',
            EC: 'Ecuador',
            EE: 'Estonia',
            EG: 'Egypt',
            EH: 'Western Sahara',
            ER: 'Eritrea',
            ES: 'Spain',
            ET: 'Ethiopia',
            FI: 'Finland',
            FJ: 'Fiji',
            FK: 'Falkland Islands',
            FM: 'Micronesia',
            FO: 'Faroe Islands',
            FR: 'France',
            GA: 'Gabon',
            GB: 'United Kingdom',
            GD: 'Grenada',
            GE: 'Georgia',
            GF: 'French Guiana',
            GG: 'Guernsey',
            GH: 'Ghana',
            GI: 'Gibraltar',
            GL: 'Greenland',
            GM: 'Gambia',
            GN: 'Guinea',
            GP: 'Guadeloupe',
            GQ: 'Equatorial Guinea',
            GR: 'Greece',
            GS: 'South Georgia and the South Sandwich Islands',
            GT: 'Guatemala',
            GU: 'Guam',
            GW: 'Guinea-Bissau',
            GY: 'Guyana',
            HK: 'Hong Kong',
            HM: 'Heard Island and McDonald Islands',
            HN: 'Honduras',
            HR: 'Croatia',
            HT: 'Haiti',
            HU: 'Hungary',
            ID: 'Indonesia',
            IE: 'Ireland',
            IL: 'Israel',
            IM: 'Isle of Man',
            IN: 'India',
            IO: 'British Indian Ocean Territory',
            IQ: 'Iraq',
            IR: 'Iran',
            IS: 'Iceland',
            IT: 'Italy',
            JE: 'Jersey',
            JM: 'Jamaica',
            JO: 'Jordan',
            JP: 'Japan',
            KE: 'Kenya',
            KG: 'Kyrgyzstan',
            KH: 'Cambodia',
            KI: 'Kiribati',
            KM: 'Comoros',
            KN: 'Saint Kitts and Nevis',
            KP: 'North Korea',
            KR: 'South Korea',
            KW: 'Kuwait',
            KY: 'Cayman Islands',
            KZ: 'Kazakhstan',
            LA: 'Lao',
            LB: 'Lebanon',
            LC: 'Saint Lucia',
            LI: 'Liechtenstein',
            LK: 'Sri Lanka',
            LR: 'Liberia',
            LS: 'Lesotho',
            LT: 'Lithuania',
            LU: 'Luxembourg',
            LV: 'Latvia',
            LY: 'Libya',
            MA: 'Morocco',
            MC: 'Monaco',
            MD: 'Moldova',
            ME: 'Montenegro',
            MF: 'Saint Martin (French part)',
            MG: 'Madagascar',
            MH: 'Marshall Islands',
            MK: 'Macedonia',
            ML: 'Mali',
            MM: 'Myanmar',
            MN: 'Mongolia',
            MO: 'Macao',
            MP: 'Northern Mariana Islands',
            MQ: 'Martinique',
            MR: 'Mauritania',
            MS: 'Montserrat',
            MT: 'Malta',
            MU: 'Mauritius',
            MV: 'Maldives',
            MW: 'Malawi',
            MX: 'Mexico',
            MY: 'Malaysia',
            MZ: 'Mozambique',
            NA: 'Namibia',
            NC: 'New Caledonia',
            NE: 'Niger',
            NF: 'Norfolk Island',
            NG: 'Nigeria',
            NI: 'Nicaragua',
            NL: 'Netherlands',
            NO: 'Norway',
            NP: 'Nepal',
            NR: 'Nauru',
            NU: 'Niue',
            NZ: 'New Zealand',
            OM: 'Oman',
            PA: 'Panama',
            PE: 'Peru',
            PF: 'French Polynesia',
            PG: 'Papua New Guinea',
            PH: 'Philippines',
            PK: 'Pakistan',
            PL: 'Poland',
            PM: 'Saint Pierre and Miquelon',
            PN: 'Pitcairn',
            PR: 'Puerto Rico',
            PS: 'Palestine',
            PT: 'Portugal',
            PW: 'Palau',
            PY: 'Paraguay',
            QA: 'Qatar',
            RE: 'Reunion',
            RO: 'Romania',
            RS: 'Serbia',
            RU: 'Russia',
            RW: 'Rwanda',
            SA: 'Saudi Arabia',
            SB: 'Solomon Islands',
            SC: 'Seychelles',
            SD: 'Sudan',
            SE: 'Sweden',
            SG: 'Singapore',
            SH: 'Saint Helena, Ascension and Tristan da Cunha',
            SI: 'Slovenia',
            SJ: 'Svalbard and Jan Mayen',
            SK: 'Slovakia',
            SL: 'Sierra Leone',
            SM: 'San Marino',
            SN: 'Senegal',
            SO: 'Somalia',
            SR: 'Suriname',
            SS: 'South Sudan',
            ST: 'Sao Tome and Principe',
            SV: 'El Salvador',
            SX: 'Sint Maarten (Dutch part)',
            SY: 'Syria',
            SZ: 'Swaziland',
            TC: 'Turks and Caicos Islands',
            TD: 'Chad',
            TF: 'French Southern Territories',
            TG: 'Togo',
            TH: 'Thailand',
            TJ: 'Tajikistan',
            TK: 'Tokelau',
            TL: 'Timor-Leste',
            TM: 'Turkmenistan',
            TN: 'Tunisia',
            TO: 'Tonga',
            TR: 'Turkey',
            TT: 'Trinidad and Tobago',
            TV: 'Tuvalu',
            TW: 'Taiwan',
            TZ: 'Tanzania',
            UA: 'Ukraine',
            UG: 'Uganda',
            UM: 'United States Minor Outlying Islands',
            US: 'United States',
            UY: 'Uruguay',
            UZ: 'Uzbekistan',
            VA: 'Holy See',
            VC: 'Saint Vincent and the Grenadines',
            VE: 'Venezuela',
            VG: 'Virgin Islands, British',
            VI: 'Virgin Islands, U.S.',
            VN: 'Viet Nam',
            VU: 'Vanuatu',
            WF: 'Wallis and Futuna',
            WS: 'Samoa',
            XK: 'Kosovo',
            YE: 'Yemen',
            YT: 'Mayotte',
            ZA: 'South Africa',
            ZM: 'Zambia',
            ZW: 'Zimbabwe'
        }
    },
    get(code, language) {
        const data = this.name[language || config.get('language') || 'english'];

        return has.call(data, code) ? data[code] : code;
    }
};
const xe = {
    exchangeRate: JSON.parse(GM_getValue('SBSE_xe', '{}')),
    currencies: {
        AUD: {
            english: 'Australian Dollar',
            tchinese: '澳幣',
            schinese: '澳元',
            symbol: 'AU$',
            decimal: true
        },
        CAD: {
            english: 'Canadian Dollar',
            tchinese: '加幣',
            schinese: '加元',
            symbol: 'CA$',
            decimal: true
        },
        CNY: {
            english: 'Chinese Yuan',
            tchinese: '人民幣',
            schinese: '人民币',
            symbol: 'CN¥',
            decimal: true
        },
        EUR: {
            english: 'Euro',
            tchinese: '歐元',
            schinese: '欧元',
            symbol: '€',
            decimal: true
        },
        GBP: {
            english: 'Great Britain Pound',
            tchinese: '英鎊',
            schinese: '英镑',
            symbol: '£',
            decimal: true
        },
        HKD: {
            english: 'Hong Kong Dollar',
            tchinese: '港幣',
            schinese: '港元',
            symbol: 'HK$',
            decimal: false
        },
        JPY: {
            english: 'Japanese Yen',
            tchinese: '日圓',
            schinese: '日元',
            symbol: 'JP¥',
            decimal: false
        },
        KRW: {
            english: 'South Korean Won',
            tchinese: '韓圓',
            schinese: '韩币',
            symbol: '₩',
            decimal: false
        },
        MYR: {
            english: 'Malaysian Ringgit',
            tchinese: '令吉',
            schinese: '林吉特',
            symbol: 'RM',
            decimal: true
        },
        NTD: {
            english: 'New Taiwan Dollar',
            tchinese: '台幣',
            schinese: '台币',
            symbol: 'NT$',
            decimal: false
        },
        NZD: {
            english: 'New Zealand Dollar',
            tchinese: '紐幣',
            schinese: '新西兰元',
            symbol: 'NZ$',
            decimal: true
        },
        RUB: {
            english: 'Russian Ruble',
            tchinese: '盧布',
            schinese: '卢布',
            symbol: '₽',
            decimal: false
        },
        USD: {
            english: 'United States Dollar',
            tchinese: '美元',
            schinese: '美元',
            symbol: 'US$',
            decimal: true
        }
    },
    getRate() {
        const self = this;

        GM_xmlhttpRequest({
            method: 'GET',
            url: 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml',
            onload: res => {
                if (res.status === 200) {
                    try {
                        const exchangeRate = {
                            lastUpdate: Date.now(),
                            rates: {}
                        };

                        res.response.split(eol).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);

                                exchangeRate.rates[currency] = parseFloat(rate);
                            }
                        });
                        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) ? exchangeRate.rates.HKD * 3.75 : rate;

                                exchangeRate.rates.NTD = NTDRate;
                                self.exchangeRate = exchangeRate;
                                GM_setValue('SBSE_xe', JSON.stringify(exchangeRate));
                            }
                        });

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

                                exchangeRate.rates.UAH = UAHRate;
                                self.exchangeRate = exchangeRate;
                                GM_setValue('SBSE_xe', JSON.stringify(exchangeRate));
                            }
                        });
                    } 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');
                }
            }
        });
    },
    update(targetCurrency = 'USD') {
        $('.SBSE_price').each((i, ele) => {
            const originalCurrency = ele.dataset.currency;
            const originalValue = parseInt(ele.dataset.value, 10);
            const originalRate = this.exchangeRate.rates[originalCurrency];
            const targetRate = this.exchangeRate.rates[targetCurrency];
            const exchangedValue = Math.trunc(originalValue / originalRate * targetRate);
            const symbol = this.currencies[targetCurrency].symbol;
            const decimalPlace = this.currencies[targetCurrency].decimal ? 2 : 0;

            $(ele).text(symbol + (exchangedValue / 100).toFixed(decimalPlace));
        });
        GM_setValue('SBSE_selectedCurrency', targetCurrency);
    },
    init() {
        const updateTimer = 12 * 60 * 60 * 1000; // update every 12 hours
        const newRate = ['UAH'];

        if (Object.keys(this.exchangeRate).length === 0 || this.exchangeRate.lastUpdate < Date.now() - updateTimer || newRate.filter(x => !has.call(this.exchangeRate.rates, x)).length > 0) this.getRate();
    }
};
const steam = {
    library: JSON.parse(GM_getValue('SBSE_steam_library', '{}')),
    games: JSON.parse(GM_getValue('SBSE_steam_games', '{}')),
    sync(a = []) {
        if (!isArray(a) || a.length === 0) {
            a.push({ key: 'library', sync: true, save: true }, { key: 'games', sync: true, save: true });
        }

        const self = this;

        a.forEach(o => {
            if (o.key === 'library' && o.sync !== false) {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://store.steampowered.com/dynamicstore/userdata/t=${Math.random()}`,
                    onload(res) {
                        const data = JSON.parse(res.response);

                        if (!isObject(self.library)) self.reset([o]);

                        self.library.owned = {
                            app: isArray(data.rgOwnedApps) ? data.rgOwnedApps : [],
                            sub: isArray(data.rgOwnedPackages) ? data.rgOwnedPackages : []
                        };
                        self.library.wished = {
                            app: isArray(data.rgWishlist) ? data.rgWishlist : [],
                            sub: []
                        };
                        self.library.ignored = {
                            app: isArray(data.rgIgnoredApps) ? data.rgIgnoredApps : [],
                            sub: isArray(data.rgIgnoredPackages) ? data.rgIgnoredPackages : []
                        };
                        self.library.lastSync = Date.now();
                        self.save([o]);

                        if (o.notify === true) {
                            swal({
                                title: i18n.get('syncSuccessTitle'),
                                text: i18n.get('syncSuccess'),
                                type: 'success',
                                timer: 3000
                            });
                        }
                    },
                    onerror() {
                        swal({
                            title: i18n.get('syncFailTitle'),
                            text: i18n.get('syncFail'),
                            type: 'error',
                            confirmButtonText: i18n.get('visitSteam'),
                            showCancelButton: true
                        }).then(result => {
                            if (result.value === true) window.open('https://store.steampowered.com/');
                        });
                    }
                });
            }
            if (o.key === 'games' && o.sync !== false) {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'http://steamspy.com/api.php?request=all',
                    onload(res) {
                        try {
                            const data = JSON.parse(res.response);

                            self.games = {
                                list: Object.keys(data).map(x => parseInt(x, 10)),
                                lastSync: Date.now()
                            };
                            self.save([o]);
                        } catch (e) {
                            throw e.stack;
                        }
                    }
                });
            }
        });
    },
    reset(a = []) {
        if (!isArray(a) || a.length === 0) {
            a.push({ key: 'library', reset: true, save: true }, { key: 'games', reset: true, save: true });
        }

        a.forEach(o => {
            if (o.key === 'library' && o.reset !== false) {
                this.library = {
                    lastSync: 0,
                    owned: { app: [], sub: [] },
                    wished: { app: [], sub: [] },
                    ignored: { app: [], sub: [] }
                };
            }
            if (o.key === 'games' && o.reset !== false) {
                this.games = {
                    lastSync: 0,
                    list: []
                };
            }

            if (o.save !== false) this.save([o]);
        });
    },
    save(a = []) {
        if (!isArray(a) || a.length === 0) {
            a.push({ key: 'library', save: true }, { key: 'games', save: true });
        }

        a.forEach(o => {
            if (has.call(this, o.key) && o.save !== false) {
                GM_setValue(`SBSE_steam_${o.key}`, JSON.stringify(this[o.key]));
                if (typeof o.callback === 'function') o.callback();
            }
        });
    },
    lastSync(key) {
        return has.call(this, key) ? this[key].lastSync : null;
    },
    isOwned(o) {
        return this.library.owned.app.includes(o.app) || this.library.owned.sub.includes(o.sub);
    },
    isWished(o) {
        return this.library.wished.app.includes(o.app) || this.library.wished.sub.includes(o.sub);
    },
    isIgnored(o) {
        return this.library.ignored.app.includes(o.app) || this.library.ignored.sub.includes(o.sub);
    },
    isGame(o) {
        return this.games.list.length > 0 && this.games.list.includes(o.app);
    },
    isDLC(o) {
        return has.call(o, 'app') && this.games.list.length > 0 && !this.games.list.includes(o.app);
    },
    isPackage(o) {
        return has.call(o, 'sub');
    },
    init() {
        if (!isObject(this.library) || !has.call(this.library, 'owned') || !has.call(this.library, 'wished') || !has.call(this.library, 'ignored')) this.reset([{ key: 'library' }]);

        if (!isObject(this.games) || !has.call(this.games, 'list')) this.reset([{ key: 'games' }]);

        // update Steam library every 10 min
        const libraryTimer = 10 * 60 * 1000;
        const libraryLastSync = this.lastSync('library');

        if (!libraryLastSync || libraryLastSync < Date.now() - libraryTimer) this.sync([{ key: 'library' }]);

        // update Steam games list every day
        const gamesTimer = 1 * 24 * 60 * 60 * 1000;
        const gamesLastSync = this.lastSync('games');

        if (!gamesLastSync || gamesLastSync < Date.now() - gamesTimer || this.games.list.length === 0) this.sync([{ key: 'games' }]);

        // delete odd values
        GM_deleteValue('SBSE_steam');
    }
};

// functions
const getSessionID = () => {
    GM_xmlhttpRequest({
        method: 'GET',
        url: 'https://store.steampowered.com/',
        onload(res) {
            if (res.status === 200) {
                const accountID = res.response.match(/g_AccountID = (\d+)/).pop();
                const sessionID = res.response.match(/g_sessionID = "(\w+)"/).pop();

                if (accountID > 0) config.set('sessionID', sessionID);else {
                    swal({
                        title: i18n.get('notLoggedInTitle'),
                        text: i18n.get('notLoggedInMsg'),
                        type: 'error',
                        showCancelButton: true
                    }).then(result => {
                        if (result.value === true) window.open('https://store.steampowered.com/');
                    });
                }
            }
        }
    });
};
const steamCNTooltip = {
    timeoutID: 0,
    load(data) {
        if (config.get('enableTooltips')) {
            const $container = $('<div/>');

            (Array.isArray(data) ? data : [data]).forEach(d => {
                ['app', 'sub'].forEach(type => {
                    if (has.call(d, type)) {
                        const url = `https://steamdb.steamcn.com/tooltip?v=4#${type}/${d[type]}#steam_info_${type}_${d[type]}_1`;

                        $container.append($(`<iframe id="tooltip_${type + d[type]}" class="SBSE_tooltip SBSE_hide" data-url="${url}"></iframe>`).mouseenter(() => {
                            clearTimeout(this.timeoutID);
                        }).mouseout(this.hide));
                    }
                });
            });

            $('body').append($container);
        }
    },
    show(e) {
        const $target = $(e.currentTarget);
        const json = $target.closest('.SBSE_processed').attr('data-gameinfo');

        if (json.length > 0 && config.get('enableTooltips')) {
            const data = JSON.parse(json);
            const opened = !!$('.SBSE_tooltip:not(.SBSE_hide)').length;

            ['app', 'sub'].forEach(type => {
                const $tooltip = $(`#tooltip_${type + data[type]}`);

                if ($tooltip.length > 0 && !opened) {
                    // load tooltip
                    if (!$tooltip.attr('src')) $tooltip.attr('src', $tooltip.attr('data-url'));

                    $tooltip.css({
                        top: e.clientY,
                        left: e.clientX + 10
                    }).removeClass('SBSE_hide');
                    this.reposition($tooltip, $tooltip.height());
                    $tooltip[0].contentWindow.postMessage('show', '*'); // get height

                    $target.one('mouseout', () => {
                        this.timeoutID = setTimeout(this.hide.bind(steamCNTooltip), 500);
                    });
                }
            });
        }
    },
    hide() {
        const $tooltip = $('.SBSE_tooltip:not(.SBSE_hide)');

        $tooltip.addClass('SBSE_hide');
        $tooltip[0].contentWindow.postMessage('hide', '*');
    },
    reposition($tooltip, height) {
        const $window = $(window);
        const $document = $(document);
        const offsetTop = $tooltip.offset().top - $document.scrollTop();
        const offsetLeft = $tooltip.offset().left - $document.scrollLeft();
        const overflowX = offsetLeft + $tooltip.width() - ($window.width() - 20);
        const overflowY = offsetTop + height - ($window.height() - 20);

        if (overflowY > 0) $tooltip.css('top', offsetTop - overflowY);
        if (overflowX > 0) $tooltip.css('left', offsetLeft - overflowX);
    },
    listen() {
        window.addEventListener('message', e => {
            if (e.origin === 'https://steamdb.steamcn.com' && e.data.height && e.data.src) {
                const $tooltip = $(`.SBSE_tooltip[src="${e.data.src}"]`);

                $tooltip.height(e.data.height);
                this.reposition($tooltip, e.data.height);
            }
        });
    }
};
const settings = {
    construct() {
        const panelHTML = `
            <div class="SBSE_settings">
                <table>
                    <tr>
                        <td class="name">${i18n.get('settingsAutoUpdateSessionID')}</td>
                        <td class="value">
                            <label class="SBSE_switch">
                                <input type="checkbox" class="autoUpdateSessionID">
                                <span class="SBSE_slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${i18n.get('settingsSessionID')}</td>
                        <td class="value">
                            <input type="text" class="sessionID" value="${config.get('sessionID')}">
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${i18n.get('settingsSyncLibrary')}</td>
                        <td class="value">
                            <button class="syncLibrary">${i18n.get('settingsSyncLibraryButton')}</button>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${i18n.get('settingsLanguage')}</td>
                        <td class="value">
                            <select class="language"></select>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${i18n.get('settingsASFFormat')}</td>
                        <td class="value">
                            <label class="SBSE_switch">
                                <input type="checkbox" class="ASFFormat">
                                <span class="SBSE_slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${i18n.get('settingsTitleComesLast')}</td>
                        <td class="value">
                            <label class="SBSE_switch">
                                <input type="checkbox" class="titleComesLast">
                                <span class="SBSE_slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${i18n.get('settingsActivateAllKeys')}</td>
                        <td class="value">
                            <label class="SBSE_switch">
                                <input type="checkbox" class="activateAllKeys">
                                <span class="SBSE_slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${i18n.get('settingsEnableTooltips')}</td>
                        <td class="value">
                            <label class="SBSE_switch">
                                <input type="checkbox" class="enableTooltips">
                                <span class="SBSE_slider"></span>
                            </label>
                        </td>
                    </tr>
                </table>
            </div>
        `;

        return panelHTML;
    },
    display() {
        swal({
            title: i18n.get('settingsTitle'),
            html: this.construct()
        });

        // apply settings
        const $panel = $(swal.getContent());
        const $sessionID = $panel.find('.sessionID');
        const $language = $panel.find('.language');

        // toggles
        $panel.find('input[type=checkbox]').each((index, input) => {
            const $input = $(input);

            $input.prop('checked', config.get(input.className));
            $input.change(e => {
                swal.showLoading();

                const setting = e.delegateTarget.className;
                const state = e.delegateTarget.checked;

                config.set(setting, state);

                if (setting === 'autoUpdateSessionID') $sessionID.attr('disabled', state);

                setTimeout(swal.hideLoading, 500);
            });
        });

        // sessionID input
        $sessionID.prop('disabled', config.get('autoUpdateSessionID'));
        $sessionID.change(() => {
            swal.showLoading();

            config.set('sessionID', $sessionID.val().trim());

            setTimeout(swal.hideLoading, 500);
        });

        // sync library
        $panel.find('.syncLibrary').click(() => {
            steam.sync([{ key: 'library', notify: true }]);
        });

        // language
        Object.keys(i18n.data).forEach(language => {
            $language.append(new Option(i18n.data[language].name, language));
        });
        $panel.find(`option[value=${config.get('language')}]`).prop('selected', true);
        $language.change(() => {
            swal.showLoading();

            const newLanguage = $language.val();
            config.set('language', newLanguage);
            i18n.set();

            setTimeout(swal.hideLoading, 500);
        });
    }
};
const activator = {
    activated: JSON.parse(GM_getValue('SBSE_activated', '[]')),
    isActivated(key) {
        return this.activated.includes(key);
    },
    pushActivated(key) {
        this.activated.push(key);
        GM_setValue('SBSE_activated', JSON.stringify(this.activated));
    },
    keyDetails: {},
    isOwned(key) {
        return has.call(this.keyDetails, key) ? this.keyDetails[key].owned : false;
    },
    pushKeyDetails(data) {
        if (!has.call(this.keyDetails, data.key)) this.keyDetails[data.key] = data;
    },
    getKeyDetails(key) {
        return has.call(this.keyDetails, key) ? this.keyDetails[key] : null;
    },
    results: [],
    resultDetails(result) {
        // result from Steam
        if (result.sbse !== true) {
            // get status
            let status = i18n.get('failStatus');
            let statusMsg = i18n.get('failDetailUnexpected');
            const errorCode = result.purchase_result_details;
            const errors = {
                14: i18n.get('failDetailInvalidKey'),
                15: i18n.get('failDetailUsedKey'),
                53: i18n.get('failDetailRateLimited'),
                13: i18n.get('failDetailCountryRestricted'),
                9: i18n.get('failDetailAlreadyOwned'),
                24: i18n.get('failDetailMissingBaseGame'),
                36: i18n.get('failDetailPS3Required'),
                50: i18n.get('failDetailGiftWallet')
            };

            if (result.success === 1) {
                status = i18n.get('successStatus');
                statusMsg = i18n.get('successDetail');
            } else if (result.success === 2) {
                if (has.call(errors, errorCode)) statusMsg = errors[errorCode];
            }

            result.status = `${status}/${statusMsg}`;

            // get description
            const info = result.purchase_receipt_info;
            const chuncks = [];

            if (info && info.line_items) {
                info.line_items.forEach(item => {
                    const chunk = [];

                    if (item.packageid > 0) chunk.push(`sub: ${item.packageid}`);
                    if (item.appid > 0) chunk.push(`app: ${item.appid}`);
                    chunk.push(item.line_item_description);

                    chuncks.push(chunk.join(' '));
                });
            }

            result.descripton = chuncks.join(', ');
        }

        const temp = [result.key];

        if (result.status) temp.push(result.status);
        if (result.descripton) temp.push(result.descripton);

        return temp.join(' | ');
    },
    activate(keys, callback) {
        this.results.length = 0;

        const updateResults = () => {
            $('.SBSE_container > textarea').val(this.results.concat(keys).join(eol));
        };
        const activateHandler = () => {
            const key = keys.shift();

            if (key) {
                if (this.isActivated(key)) {
                    this.results.push(this.resultDetails({
                        sbse: true,
                        key,
                        status: `${i18n.get('skippedStatus')}/${i18n.get('activatedDetail')}`,
                        descripton: i18n.get('noItemDetails')
                    }));
                    updateResults();

                    // next key
                    activateHandler();
                } else if (this.isOwned(key) && !config.get('activateAllKeys')) {
                    const detail = this.getKeyDetails(key);
                    const description = [];

                    ['app', 'sub'].forEach(type => {
                        if (has.call(detail, type)) description.push(`${type}: ${detail[type]} ${detail.title}`);
                    });

                    this.results.push(this.resultDetails({
                        sbse: true,
                        key,
                        status: `${i18n.get('skippedStatus')}/${i18n.get('failDetailAlreadyOwned')}`,
                        descripton: description.join()
                    }));
                    updateResults();

                    // next key
                    activateHandler();
                } else {
                    const self = this;

                    GM_xmlhttpRequest({
                        method: 'POST',
                        url: 'https://store.steampowered.com/account/ajaxregisterkey/',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                            Origin: 'https://store.steampowered.com',
                            Referer: 'https://store.steampowered.com/account/registerkey'
                        },
                        data: `product_key=${key}&sessionid=${config.get('sessionID')}`,
                        onload(res) {
                            if (res.status === 200) {
                                const result = JSON.parse(res.response);

                                // update activated
                                const failCode = result.purchase_result_details;
                                if (result.success === 1 || [14, 15, 9].includes(failCode)) {
                                    self.pushActivated(key);

                                    // dispatch activated event
                                    $(document).trigger('activated', [key, result]);
                                }

                                result.key = key;
                                self.results.push(self.resultDetails(result));
                                updateResults();

                                // next key
                                setTimeout(activateHandler.bind(self), 2000);
                            } else {
                                const errorMsg = [];

                                errorMsg.push('<pre class="SBSE_errorMsg">');
                                errorMsg.push(`sessionID: ${config.get('sessionID') + eol}`);
                                errorMsg.push(`autoUpdate: ${config.get('autoUpdateSessionID') + eol}`);
                                errorMsg.push(`status: ${res.status + eol}`);
                                errorMsg.push(`response: ${res.response + eol}`);
                                errorMsg.push('</pre>');

                                swal({
                                    title: i18n.get('failTitle'),
                                    html: i18n.get('failDetailRequestFailedNeedUpdate') + eol + errorMsg.join(''),
                                    type: 'error'
                                });
                                getSessionID();
                                if (typeof callback === 'function') callback();
                            }
                        }
                    });
                }
            } else if (typeof callback === 'function') callback();
        };

        activateHandler();
    }
};
const getContainer = handlers => {
    const $container = $(`
        <div class="SBSE_container">
            <textarea></textarea>
            <div>
                <button class="SBSE_BtnReveal">${i18n.get('buttonReveal')}</button>
                <button class="SBSE_BtnRetrieve">${i18n.get('buttonRetrieve')}</button>
                <button class="SBSE_BtnActivate">${i18n.get('buttonActivate')}</button>
                <button class="SBSE_BtnCopy">${i18n.get('buttonCopy')}</button>
                <button class="SBSE_BtnReset">${i18n.get('buttonReset')}</button>
                <div class="SBSE_ExportMenu SBSE_dropdown">
                    <button class="SBSE_BtnExport">${i18n.get('buttonExport')}</button>
                    <ul class="SBSE_ExportList SBSE_dropdown_list">
                        <li><a data-fileType="txt">.txt</a></li>
                        <li><a data-fileType="csv">.csv</a></li>
                        <li><a data-fileType="keys">.keys</a></li>
                    </ul>
                </div>
                <label><input type="checkbox" class="SBSE_ChkTitle">${i18n.get('checkboxIncludeGameTitle')}</label>
                <label><input type="checkbox" class="SBSE_ChkJoin">${i18n.get('checkboxJoinKeys')}</label>
                <select class="SBSE_SelFilter">
                    <option value="All" selected>${i18n.get('selectFilterAll')}</option>
                    <option value="Owned">${i18n.get('selectFilterOwned')}</option>
                    <option value="NotOwned">${i18n.get('selectFilterNotOwned')}</option>
                </select>
                <button id="SBSE_BtnSettings"> </button>
            </div>
        </div>
    `);

    if (typeof handlers.reveal !== 'function') handlers.reveal = () => {};
    if (typeof handlers.retrieve !== 'function') {
        handlers.retrieve = () => {
            const data = handlers.extract();
            const keys = [];
            const includeTitle = !!$('.SBSE_ChkTitle:checked').length;
            const joinKeys = !!$('.SBSE_ChkJoin:checked').length;
            const selected = $('.SBSE_SelFilter').val() || 'All';
            const skipUsed = !!$('.SBSE_ChkSkipUsed:checked').length;
            const skipMarketListing = !$('.SBSE_ChkMarketListings:checked').length;
            const separator = joinKeys ? ',' : eol;
            const prefix = joinKeys && config.get('ASFFormat') ? '!redeem ' : '';

            for (let i = 0; i < data.items.length; i += 1) {
                const item = data.items[i];

                if (selected === 'Owned' && !item.owned) continue;
                if (selected === 'NotOwned' && item.owned) continue;
                if (skipUsed && item.used) continue;
                if (skipMarketListing && item.marketListing) continue;

                const temp = [item.key];

                if (config.get('ASFFormat')) {
                    if (!joinKeys) temp.unshift(item.title);

                    keys.push(temp.join(tab));
                } else {
                    if (includeTitle) temp.unshift(item.title);
                    if (config.get('titleComesLast')) temp.reverse();

                    keys.push(temp.join(', '));
                }
            }

            $('.SBSE_container > textarea').val(prefix + keys.join(separator));
        };
    }
    if (typeof handlers.activate !== 'function') {
        handlers.activate = e => {
            const $textarea = $container.find('textarea');
            const keys = unique($textarea.val().match(regKey));

            if (keys.length > 0) {
                const $activateBtn = $(e.currentTarget);

                $activateBtn.prop('disabled', true).addClass('working');
                $textarea.attr('disabled', '');

                $textarea.val(keys.join(eol));
                activator.activate(keys, () => {
                    $activateBtn.prop('disabled', false).removeClass('working');
                    $textarea.removeAttr('disabled');
                });
            } else $textarea.val(i18n.get('emptyInput'));
        };
    }
    if (typeof handlers.copy !== 'function') {
        handlers.copy = () => {
            $('.SBSE_container > textarea').select();
            document.execCommand('copy');
        };
    }
    if (typeof handlers.reset !== 'function') {
        handlers.reset = () => {
            $('.SBSE_container > textarea').val('');
        };
    }
    if (typeof handlers.export !== 'function') {
        handlers.export = e => {
            const data = handlers.extract();

            if (data.items.length > 0) {
                const exportBtn = e.target;

                exportBtn.removeAttribute('href');
                exportBtn.removeAttribute('download');

                const fileType = exportBtn.dataset.filetype || 'txt';
                const filename = data.filename.replace(/[\\/:*?"<>|!]/g, '');
                const separator = {
                    txt: ', ',
                    csv: ',',
                    keys: tab
                };
                const formattedData = data.items.map(line => {
                    const temp = [];

                    if (line.title) temp.push(line.title.replace(/,/g, ' '));
                    temp.push(line.key);

                    return temp.join(separator[fileType]);
                }).join(eol);

                exportBtn.href = `data:text/${fileType};charset=utf-8,\ufeff${encodeURIComponent(formattedData)}`;
                exportBtn.download = `${filename}.${fileType}`;
            }
        };
    }
    if (typeof handlers.settings !== 'function') {
        handlers.settings = () => {
            settings.display();
        };
    }

    // bind event
    $container.find('button').click(e => {
        e.preventDefault();
    });
    $container.find('.SBSE_BtnReveal').click(handlers.reveal);
    $container.find('.SBSE_BtnRetrieve').click(handlers.retrieve);
    $container.find('.SBSE_BtnActivate').click(handlers.activate);
    $container.find('.SBSE_BtnCopy').click(handlers.copy);
    $container.find('.SBSE_BtnReset').click(handlers.reset);
    $container.find('.SBSE_ExportList').click(handlers.export);
    $container.find('#SBSE_BtnSettings').click(handlers.settings);
    $container.find('input[type="checkbox"]').change(e => {
        const key = e.currentTarget.className.trim();

        if (key.length > 0) config.set(key, e.currentTarget.checked);
    });

    // apply settings
    if (config.get('SBSE_ChkTitle')) $container.find('.SBSE_ChkTitle').prop('checked', 'checked');
    if (config.get('SBSE_ChkJoin')) $container.find('.SBSE_ChkJoin').prop('checked', 'checked');

    return $container;
};
const siteHandlers = {
    indiegala() {
        // inject css
        GM_addStyle(`
            .SBSE_container { margin-top: 10px; }
            .SBSE_container > textarea { border: 1px solid #CC001D; border-radius: 3px; }
            .SBSE_container > div button { width: 100px; background-color: #CC001D; color: white; border-radius: 3px; }
            .swal2-popup .SBSE_slider { margin: 0; }
            .SBSE_icon { vertical-align: middle; }
        `);

        const handlers = {
            extract() {
                const source = location.pathname === '/profile' ? 'div[id*="_sale_"].collapse.in' : document;
                const bundleTitle = $('[aria-expanded="true"] > div#bundle-title, #bundle-title, #indie_gala_2 > div > span').eq(0).text().trim();
                const data = {
                    title: bundleTitle,
                    filename: `IndieGala ${bundleTitle} Keys`,
                    items: []
                };

                $(source).find('.game-key-string').each((i, ele) => {
                    const $ele = $(ele);
                    const key = $ele.find('.keys').val();

                    if (key) {
                        const d = JSON.parse($(ele).closest('.SBSE_processed').attr('data-gameinfo') || '{}');

                        if (Object.keys(d).length === 0) {
                            const $a = $ele.find('.title_game > a');
                            const matched = $a.attr('href').match(/steam.+\/(app|sub)\/(\d+)/);

                            d.title = $a.text().trim();
                            if (matched) d[matched[1]] = parseInt(matched[2], 10);
                        }

                        d.key = key;

                        activator.pushKeyDetails(d);
                        data.items.push(d);
                    }
                });

                return data;
            },
            reveal(e) {
                const source = location.pathname === '/profile' ? 'div[id*="_sale_"].collapse.in' : document;
                const $revealBtn = $(e.currentTarget);
                const selected = $('.SBSE_SelFilter').val() || 'All';
                const handler = ($games, callback) => {
                    const game = $games.shift();

                    if (game) {
                        const $game = $(game);
                        const code = $game.attr('id').split('_').pop();
                        const id = $game.attr('onclick').match(/steampowered\.com\/(app|sub)\/(\d+)/)[2];
                        const d = JSON.parse($game.closest('.SBSE_processed').attr('data-gameinfo') || '{}');

                        if (selected === 'All' || selected === 'Owned' && d.owned || selected === 'NotOwned' && !d.owned) {
                            $.ajax({
                                method: 'GET',
                                url: '/myserials/syncget',
                                dataType: 'json',
                                data: {
                                    code,
                                    cache: false,
                                    productId: id
                                },
                                beforeSend() {
                                    $(`#permbutton_${code}, #fetchlink_${code}, #info_key_${code}`).hide();
                                    $(`#fetching_${code}`).fadeIn();
                                    $(`#ajax_loader_${code}`).show();
                                    $(`#container_activate_${code}`).html('');
                                },
                                success(data) {
                                    $(`#ajax_loader_${code}, #fetching_${code}, #info_key_${code}`).hide();
                                    $(`#serial_${code}`).fadeIn();
                                    $(`#serial_n_${code}`).val(data.serial_number);
                                    $game.parent().prev().find('.btn-convert-to-trade').remove();

                                    handler($games, callback);
                                },
                                error() {
                                    swal(i18n.get('failTitle'), i18n.get('failDetailUnexpected'), 'error');
                                }
                            });
                        } else handler($games, callback);
                    } else callback();
                };

                $revealBtn.addClass('working');

                handler($(source).find('a[id^=fetchlink_]'), () => {
                    $revealBtn.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                });
            }
        };
        const process = () => {
            const tooltipsData = [];

            $('.game-key-string').each((i, ele) => {
                const $ele = $(ele);
                const $a = $ele.find('.title_game > a');
                const d = {
                    title: $a.text().trim()
                };

                const matched = $a.attr('href').match(/steam.+\/(app|sub)\/(\d+)/);
                if (matched) d[matched[1]] = parseInt(matched[2], 10);

                // check if owned & wished
                d.owned = steam.isOwned(d);
                d.wished = steam.isWished(d);

                if (d.owned) $ele.addClass('SBSE_owned');
                if (d.wished) $ele.addClass('SBSE_wished');

                // append icon
                $a.after($('<span class="SBSE_icon"></span>').mouseenter(steamCNTooltip.show.bind(steamCNTooltip)));

                tooltipsData.push(d);

                $ele.attr('data-gameinfo', JSON.stringify(d)).addClass('SBSE_processed isSteam');
            });

            // load SteamCN tooltip
            steamCNTooltip.load(tooltipsData);
        };
        const $container = getContainer(handlers);

        process();

        // insert container
        $('#library-contain').eq(0).before($container);

        // support for new password protected gift page
        const $node = $('#gift-contents');

        if ($node.length > 0) {
            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach(addedNode => {
                        if (addedNode.id === 'library-contain') {
                            process();
                            $node.prepend($container);
                            observer.disconnect();
                        }
                    });
                });
            });

            observer.observe($node[0], { childList: true });
        }
    },
    fanatical() {
        // inject css
        GM_addStyle(`
            .SBSE_container { margin-top: 10px; }
            .SBSE_container > textarea { background-color: #434343; color: #eee; }
            .SBSE_container > div button { width: 80px; }
            .SBSE_container > div button, .SBSE_container select { border: 1px solid transparent; background-color: #1c1c1c; color: #eee; }
            .SBSE_container > div button:hover, .SBSE_container select:hover { color: #A8A8A8; }
            .SBSE_container label { color: #DEDEDE; }
            .SBSE_container span { margin-right: 0; margin-left: 10px; float: right; }
            .SBSE_container span { margin-top: 5px; }

            /* currency converter */
            .priceExt { positon: relative; }
            .priceExt ~ .priceExt { display: none; }
            .portrait { width: 100%; padding: 0 .875rem 0 .875rem; }
            .portrait > div { padding: 1rem; }
            .portrait .currencyToggler {
                width: 100%; height: 40px;
                margin-bottom: 10px;
                font-size: 20px;
                border-radius: 3px;
            }
            .landscape { padding: 1rem; }
            .landscape > div { display: flex; align-items: center; justify-content: space-evenly; }
            .landscape .currencyToggler {
                width: 300px; height: 40px;
                font-size: 20px;
                border-radius: 3px;
            }
            .pricingDetail { background-color: transparent; }
            .pricingDetail th { padding-top: 10px; }
            .pricingDetail .cheapest { border-bottom: 1px solid #ff9800; font-weight: bold; }
            .pricingDetail .currency-flag { vertical-align: text-bottom; }
            .swal2-popup table { background-color: white; }
            .SBSE_icon { vertical-align: bottom; }
        `);

        let APIData = null;
        const fetchAPIData = (() => {
            var _ref = _asyncToGenerator(function* (s, c) {
                let slug = s;
                let callback = c;
                if (typeof s === 'function') {
                    callback = s;
                    slug = location.href.split('/').pop();
                }

                let JSONString = GM_getValue(`Fanatical-${slug}`, '');

                if (JSONString.length === 0) {
                    const res = yield fetch(`https://www.fanatical.com/api/products/${slug}`);

                    if (res.ok) {
                        JSONString = yield res.text();

                        GM_setValue(`Fanatical-${slug}`, JSONString);
                    } else JSONString = '{}';
                }

                APIData = JSON.parse(JSONString);

                if (typeof callback === 'function') callback();
            });

            return function fetchAPIData(_x, _x2) {
                return _ref.apply(this, arguments);
            };
        })();
        const productHandler = (() => {
            var _ref2 = _asyncToGenerator(function* () {
                if (Object.keys(APIData).length > 0) {
                    const language = config.get('language');
                    const $priceExt = $(`
                    <div class="priceExt portrait">
                        <div>
                            <select class="currencyToggler"></select>
                        </div>
                    </div>
                `);
                    const $currencyToggler = $priceExt.find('.currencyToggler');
                    const $pricingDetail = $('<table class="pricingDetail"></table>');
                    const selectedCurrency = GM_getValue('SBSE_selectedCurrency', 'USD');
                    const isStarDeal = !!$('.stardeal-purchase-info').length;
                    let starDeal = {};

                    if (isStarDeal) {
                        // fetch star-deal data
                        const res = yield fetch('https://www.fanatical.com/api/star-deal');

                        if (res.ok) starDeal = yield res.json();
                    }

                    // change orientation
                    if (isStarDeal || $('.background-bundle, .bundle-header.container-fluid').length > 0) {
                        $priceExt.toggleClass('portrait landscape container');
                    }

                    Object.keys(xe.currencies).forEach(function (currency) {
                        const selected = currency === selectedCurrency ? ' selected' : '';

                        $currencyToggler.append($(`<option value="${currency}"${selected}>${xe.currencies[currency][language]}</option>`));
                    });

                    $currencyToggler.change(function () {
                        xe.update($currencyToggler.val());
                    });

                    // bundle page
                    APIData.bundles.forEach(function (tier, index) {
                        const $detail = $pricingDetail.clone();

                        if (APIData.bundles.length > 1) $detail.append(`<tr><th colspan="3">Tier ${index + 1}</th></tr>`);
                        Object.keys(tier.price).sort().forEach(function (currency) {
                            const value = tier.price[currency];
                            const symbol = xe.currencies[currency].symbol;
                            const decimalPlace = xe.currencies[currency].decimal ? 2 : 0;

                            $detail.append(`
                            <tr class="tier${index + 1}">
                                <td><div class="currency-flag currency-flag-${currency.toLowerCase()}"></div></td>
                                <td>${symbol + (value / 100).toFixed(decimalPlace)}</td>
                                <td> ≈ <span class="SBSE_price" data-currency="${currency}" data-value="${value}"></span></td>
                            </tr>
                        `);
                        });

                        $detail.appendTo($currencyToggler.parent());
                    });

                    // game page
                    if (location.href.includes('/game/') || location.href.includes('/dlc/')) {
                        let discount = 1;

                        if (has.call(APIData, 'current_discount') && new Date(APIData.current_discount.until).getTime() > Date.now()) discount = 1 - APIData.current_discount.percent;

                        if (isStarDeal) discount = 1 - $('.discount-percent').text().replace(/\D/g, '') / 100;

                        Object.keys(APIData.price).sort().forEach(function (currency) {
                            let value = Math.trunc(APIData.price[currency] * discount);
                            const symbol = xe.currencies[currency].symbol;
                            const decimalPlace = xe.currencies[currency].decimal ? 2 : 0;

                            // if star-deal data loaded successfully
                            if (has.call(starDeal, 'promoPrice')) value = starDeal.promoPrice[currency];

                            $pricingDetail.append(`
                            <tr class="tier1">
                                <td><div class="currency-flag currency-flag-${currency.toLowerCase()}"></div></td>
                                <td>${symbol + (value / 100).toFixed(decimalPlace)}</td>
                                <td> ≈ <span class="SBSE_price" data-currency="${currency}" data-value="${value}"></span></td>
                            </tr>
                        `).appendTo($currencyToggler.parent());
                        });
                    }

                    $('.product-commerce-container').append($priceExt);
                    $('.stardeal-purchase-info, .bundle-header').filter(':visible').eq(0).after($priceExt);
                    xe.update(selectedCurrency);

                    // highlight the cheapest
                    for (let i = 1; i < 10; i += 1) {
                        const $prices = $(`.tier${i} .SBSE_price`);

                        if ($prices.length === 0) break;

                        $($prices.toArray().sort(function (a, b) {
                            return a.textContent.replace(/\D/g, '') - b.textContent.replace(/\D/g, '');
                        }).shift()).closest('tr').addClass('cheapest');
                    }
                }
            });

            return function productHandler() {
                return _ref2.apply(this, arguments);
            };
        })();
        const handlers = {
            extract() {
                const bundleTitle = $('h5').eq(0).text().trim();
                const data = {
                    title: bundleTitle,
                    filename: `Fanatical ${bundleTitle} Keys`,
                    items: []
                };

                $('.account-content dl:has(input)').each((i, dl) => {
                    const $dl = $(dl);
                    const key = $dl.find('input').val();

                    if (key) {
                        const d = JSON.parse($dl.closest('.SBSE_processed').attr('data-gameinfo') || '{}');

                        if (Object.keys(d).length === 0) {
                            d.title = $dl.find('dd').eq(1).text().trim();
                        }

                        d.key = key;

                        activator.pushKeyDetails(d);
                        data.items.push(d);
                    }
                });

                return data;
            },
            reveal(e) {
                const $revealBtn = $(e.currentTarget);
                const selected = $('.SBSE_SelFilter').val() || 'All';
                const handler = ($games, callback) => {
                    const game = $games.shift();

                    if (game) {
                        const d = JSON.parse($(game).closest('.SBSE_processed').attr('data-gameinfo') || '{}');

                        if (selected === 'All' || selected === 'Owned' && d.owned || selected === 'NotOwned' && !d.owned) {
                            game.click();
                            setTimeout(handler.bind(null, $games, callback), 300);
                        } else setTimeout(handler.bind(null, $games, callback), 1);
                    } else setTimeout(callback, 500);
                };

                $revealBtn.addClass('working');

                handler($('.account-content dl button'), () => {
                    $revealBtn.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                });
            }
        };
        const process = $node => {
            const title = $node.find('h5').eq(0).text();
            const slug = title.trim().toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '');

            // empty textarea
            $('.SBSE_container textarea').val('');

            if (slug.length > 0) {
                fetchAPIData(slug, () => {
                    if (Object.keys(APIData).length > 0) {
                        const tooltipsData = [];
                        const matchGame = data => {
                            if (has.call(data, 'steam') && data.steam.id) {
                                const $gameTitle = $node.find(`dd > div:contains(${data.name})`).filter((index, name) => data.name === name.textContent.trim());
                                const $dl = $gameTitle.closest('dl');
                                const d = {
                                    title: data.name,
                                    app: parseInt(data.steam.id, 10)
                                };

                                d.owned = steam.isOwned(d);
                                d.wished = steam.isWished(d);

                                // check if owned & wished
                                if (d.owned) $dl.addClass('SBSE_owned');
                                if (d.wished) $dl.addClass('SBSE_wished');

                                // append Steam store link
                                $gameTitle.find('.game-name').append(`<span> | </span><a class="SBSE_steamStore" href="https://store.steampowered.com/app/${d.app}/" target="_blank">${i18n.get('steamStore')}</a>`, $('<span class="SBSE_icon"></span>').mouseenter(steamCNTooltip.show.bind(steamCNTooltip)));

                                tooltipsData.push(d);

                                $dl.addClass('SBSE_processed isSteam').attr('data-gameinfo', JSON.stringify(d));
                            }
                        };

                        matchGame(APIData);
                        APIData.bundles.forEach(tier => {
                            tier.games.forEach(matchGame);
                        });

                        // load SteamCN tooltip
                        steamCNTooltip.load(tooltipsData);
                    }
                });
            }
        };
        const $container = getContainer(handlers);

        $container.find('button').addClass('narrow'); // narrow buttons
        $container.find('a').attr('href', ''); // dodge from master css selector

        new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                Array.from(mutation.addedNodes).filter(x => x.nodeType === 1).forEach(node => {
                    const $node = $(node);

                    // url changed
                    if (node.matches('[property="og:url"]')) {
                        const currentURL = location.href;

                        if (currentURL.includes('/orders/')) {
                            // insert container
                            const $anchor = $('h3:contains(Order Keys)');

                            if ($('.SBSE_container').length === 0 && $anchor.length > 0) $anchor.eq(0).before($container);
                        }
                        if (currentURL.includes('/bundle/') || currentURL.includes('/game/') || currentURL.includes('/dlc/')) fetchAPIData(productHandler);
                    }

                    // order contents loaded
                    if ($node.find('dl').length > 0) process($node);
                });
            });
        }).observe($('html')[0], {
            childList: true,
            subtree: true
        });
    },
    humblebundle() {
        // inject css
        GM_addStyle(`
            .SBSE_container > div { position: relative; }
            .SBSE_container > textarea {
                border: 1px solid #CFCFCF;
                border-radius: 5px;
                color: #4a4c45;
                text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
            }
            .SBSE_container > div button {
                width: 70px;
                border: 1px solid #C9CCD3;
                border-radius: 3px;
                background-color: #C5C5C5;
                background: linear-gradient(to top, #cacaca, #e7e7e7);
                color: #4a4c45 !important;
            }
            .SBSE_container > div button:hover {
                border: 1px solid #b7bac0;
                background-color: #fafcff;
                color: #555961 !important;
            }
            .SBSE_container > div button.narrow.working { width: 76px; padding-right: 36px; }
            #SBSE_BtnSettings { position: absolute; right: 0; }
            .SBSE_owned .sr-unredeemed-steam-button {
                background-color: #F3F3F3;
                background: linear-gradient(to top, #E8E8E8, #F6F6F6);
            }/*
            .SBSE_owned .heading-text h4 > span:not(.steam-owned):last-child::after {
                content: '\\f085';
                font-family: hb-icons;
                color: #17A1E5;
            }*/
            .SBSE_activationRestrictions { float: right; margin-right: 5px; cursor: pointer; }
            .swal2-icon-text { font-size: inherit; }
            .flag-icon { width: 4em; height: 3em; border-radius: 3px; }
            .flag-icon-unknown { border: 1px solid; text-align: center; line-height: 3em; }
            .key-redeemer h4 { position: relative; }
            .key-redeemer .SBSE_icon { position: absolute; top: 50%; margin-top: -10px; }
        `);

        let gamekey;
        const atDownload = location.pathname === '/downloads';
        const fetchKey = (() => {
            var _ref3 = _asyncToGenerator(function* ($node, machineName, callback) {
                if (gamekey) {
                    const res = yield fetch('https://www.humblebundle.com/humbler/redeemkey', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                            Origin: 'https://www.humblebundle.com',
                            Referer: location.href
                        },
                        body: `keytype=${machineName}&key=${gamekey}&keyindex=0`,
                        credentials: 'same-origin'
                    });

                    if (res.ok) {
                        const d = yield res.json();

                        if (d.success) {
                            $node.closest('.container').html(`
                            <div title="${d.key}" class="keyfield redeemed">
                                <div class="keyfield-value">${d.key}</div>
                                <a class="steam-redeem-button" href="https://store.steampowered.com/account/registerkey?key=${d.key}" target="_blank">
                                    <div class="steam-redeem-text">Redeem</div>
                                    <span class="tooltiptext">Redeem on Steam</span>
                                </a>
                                <div class="spinner"></div>
                            </div>
                        `);
                        } else swal(i18n.get('failTitle'), JSON.stringify(d), 'error');
                    } else $node.click();

                    if (typeof callback === 'function') callback();
                } else $node.click();
            });

            return function fetchKey(_x3, _x4, _x5) {
                return _ref3.apply(this, arguments);
            };
        })();
        const handlers = {
            extract() {
                const bundleTitle = $('title').text().split(' (').shift();
                const data = {
                    title: bundleTitle,
                    filename: `Humble Bundle ${bundleTitle} Keys`,
                    items: []
                };

                $('.keyfield.redeemed .keyfield-value').each((i, ele) => {
                    const $ele = $(ele);
                    const key = $ele.text().trim();

                    if (key) {
                        const d = JSON.parse($ele.closest('.SBSE_processed').attr('data-gameinfo') || '{}');

                        if (Object.keys(d).length === 0) {
                            const $titleEle = $ele.closest(atDownload ? '.container' : '.redeemer-cell').prev().find('h4');

                            d.title = $titleEle.contents().eq(0).text().trim();
                        }

                        d.key = key;

                        activator.pushKeyDetails(d);
                        data.items.push(d);
                    }
                });

                return data;
            },
            reveal(e) {
                const $revealBtn = $(e.currentTarget);
                const selected = $('.SBSE_SelFilter').val() || 'All';
                const handler = ($games, callback) => {
                    const game = $games.shift();

                    if (game) {
                        const $game = $(game);
                        const machineName = $game.closest('.key-redeemer').attr('data-machineName');
                        const d = JSON.parse($(game).closest('.SBSE_processed').attr('data-gameinfo') || '{}');

                        if (atDownload && machineName) {
                            if (selected === 'All' || selected === 'Owned' && d.owned || selected === 'NotOwned' && !d.owned) {
                                fetchKey($game, machineName, () => {
                                    handler($games, callback);
                                });
                            } else setTimeout(handler.bind(null, $games, callback), 1);
                        } else {
                            game.click();
                            $('.sr-warning-modal-confirm-button').click();

                            setTimeout(handler.bind(null, $games, callback), 200);
                        }
                    } else callback();
                };

                $revealBtn.addClass('working');

                handler($('.key-redeemer.isSteam .keyfield:not(.redeemed)'), () => {
                    $revealBtn.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                });
            }
        };
        const process = (() => {
            var _ref4 = _asyncToGenerator(function* ($node) {
                gamekey = new URLSearchParams(location.search).get('key');
                let json = GM_getValue(gamekey, '');

                if (json.length === 0) {
                    const res = yield fetch(`https://www.humblebundle.com/api/v1/order/${gamekey}?all_tpkds=true`, {
                        method: 'GET',
                        credentials: 'same-origin'
                    });

                    if (res.ok) json = yield res.text();
                }

                try {
                    const data = JSON.parse(json);
                    const tooltipsData = [];

                    data.tpkd_dict.all_tpks.forEach(function (game) {
                        const $keyRedeemer = $node.find(`.key-redeemer:has(.heading-text[data-title="${game.human_name}"])`);

                        if ($keyRedeemer.length > 0) {
                            if (game.key_type === 'steam') {
                                $keyRedeemer.addClass('isSteam');

                                const d = {
                                    title: game.human_name,
                                    app: parseInt(game.steam_app_id, 10)
                                };

                                d.owned = steam.isOwned(d);
                                d.wished = steam.isWished(d);

                                // apply owned effect on game title
                                if (d.owned) $keyRedeemer.addClass('SBSE_owned');
                                if (d.wished) $keyRedeemer.addClass('SBSE_wished');

                                // store data
                                $keyRedeemer.attr({
                                    'data-machineName': game.machine_name,
                                    'data-humanName': game.human_name,
                                    'data-gameinfo': JSON.stringify(d)
                                });

                                // append Steam store link
                                $keyRedeemer.find('h4 > span').eq(0).after(`<span> | </span><a class="SBSE_steamStore" href="https://store.steampowered.com/app/${d.app}/" target="_blank">${i18n.get('steamStore')}</a>`);

                                tooltipsData.push(d);
                            }

                            // activation restrictions
                            let html = '';
                            const disallowed = game.disallowed_countries.map(function (c) {
                                return ISO2.get(c);
                            });
                            const exclusive = game.exclusive_countries.map(function (c) {
                                return ISO2.get(c);
                            });
                            const separator = config.get('language').includes('chinese') ? '、' : ', ';

                            if (disallowed.length > 0) html += `<p>${i18n.get('HBDisallowedCountries')}<br>${disallowed.join(separator)}</p>`;
                            if (exclusive.length > 0) html += `<p>${i18n.get('HBExclusiveCountries')}<br>${exclusive.join(separator)}</p>`;
                            if (disallowed.length > 0 || exclusive.length > 0) {
                                $(`<span class="SBSE_activationRestrictions">${i18n.get('HBActivationRestrictions')}</span>`).click(function () {
                                    swal({
                                        title: `${game.human_name}<br>${i18n.get('HBActivationRestrictions')}`,
                                        html,
                                        type: 'info'
                                    });
                                }).insertBefore($keyRedeemer.find('.heading-text > h4'));
                            }

                            $keyRedeemer.addClass('SBSE_processed');
                        }
                    });

                    // override default popups
                    document.addEventListener('click', function (e) {
                        const $target = $(e.target).closest('.keyfield:not(.redeemed)');
                        const $keyRedeemer = $target.closest('.key-redeemer.isSteam');
                        const machineName = $keyRedeemer.attr('data-machineName');

                        if ($target.length > 0 && $keyRedeemer.length > 0 && machineName) {
                            e.stopPropagation();

                            if ($keyRedeemer.hasClass('SBSE_owned')) {
                                swal({
                                    title: i18n.get('HBAlreadyOwned'),
                                    text: i18n.get('HBRedeemAlreadyOwned').replace('%title%', $keyRedeemer.attr('data-humanName')),
                                    type: 'question',
                                    showCancelButton: true
                                }).then(function (result) {
                                    if (result.value) fetchKey($target, machineName);
                                });
                            } else fetchKey($target, machineName);
                        }
                    }, true);

                    // load SteamCN tooltip
                    steamCNTooltip.load(tooltipsData);
                } catch (e) {
                    throw e;
                }
            });

            return function process(_x6) {
                return _ref4.apply(this, arguments);
            };
        })();
        const $container = getContainer(handlers);
        const $keyManager = $('.js-key-manager-holder');

        // narrow buttons
        $container.find('div > button').addClass('narrow');

        // at home page
        if ($keyManager.length > 0) {
            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach(addedNode => {
                        if (addedNode.className === 'header') {
                            observer.disconnect();
                            $(addedNode).after($container);
                        }
                    });
                });
            });

            observer.observe($keyManager[0], { childList: true });
            // at download page
        } else {
            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach((() => {
                        var _ref5 = _asyncToGenerator(function* (addedNode) {
                            const $node = $(addedNode);

                            if ($node.hasClass('key-list') || $node.find('.key-list').length > 0) {
                                observer.disconnect();
                                $node.closest('.whitebox-redux').before($container);

                                // fetch game heading & wrap heading
                                $node.find('.heading-text > h4').each(function (i, heading) {
                                    heading.parentElement.dataset.title = heading.innerText.trim();
                                    $(heading.firstChild).wrap('<span/>');
                                    $(heading).append($('<span class="SBSE_icon"></span>').mouseenter(steamCNTooltip.show.bind(steamCNTooltip)));
                                });

                                // fetch & process key data
                                process($node);
                            }
                        });

                        return function (_x7) {
                            return _ref5.apply(this, arguments);
                        };
                    })());
                });
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        // append user's region
        const countryCode = unsafeWindow.models.request.country_code;

        if (countryCode) {
            const code = countryCode.toLowerCase();
            const countryName = ISO2.get(countryCode);
            const $flag = $(`<span class="flag-icon flag-icon-unknown" tooltip="${i18n.get('HBCurrentLocation')}?"></span>`);

            if (GM_getResourceText('flagIcon').includes(`${code}.svg`)) {
                $flag.toggleClass(`flag-icon-unknown flag-icon-${code}`).attr('tooltip', i18n.get('HBCurrentLocation') + countryName);
            } else $flag.text('?');

            $('.navbar-content').prepend($flag);
        }
    },
    dailyindiegame() {
        const MPHideList = JSON.parse(GM_getValue('SBSE_DIGMPHideList') || '[]');
        const pathname = location.pathname;

        if (pathname.includes('/account_page') || pathname.includes('/account_update')) {
            // force sync library
            steam.sync([{ key: 'library' }]);

            // inject css
            GM_addStyle(`
                .SBSE_container { padding: 5px; border: 1px solid #424242; }
                .SBSE_container > textarea { border: 1px solid #000; }
                .SBSE_container > div button {
                    border: none;
                    background-color: #FD5E0F;
                    color: rgb(49, 49, 49);
                    font-family: Ropa Sans;
                    font-size: 15px;
                    font-weight: 600;
                }
            `);

            const handlers = {
                extract() {
                    const data = {
                        title: 'DailyIndieGame Keys',
                        filename: 'DailyIndieGame Keys',
                        items: []
                    };

                    $('#TableKeys tr').each((i, tr) => {
                        const $tds = $(tr).children();
                        const key = $tds.eq(4).text().trim();

                        if (key.includes('-')) {
                            const d = {
                                title: $tds.eq(2).text().trim(),
                                key,
                                marketListing: $tds.eq(6).text().includes('Cancel trade')
                            };

                            activator.pushKeyDetails(d);
                            data.items.push(d);
                        }
                    });

                    return data;
                },
                reveal() {
                    const $form = $('#form3');

                    $('#quickaction').val(1);
                    $.ajax({
                        method: 'POST',
                        url: $form.attr('action'),
                        data: $form.serializeArray(),
                        success() {
                            location.reload();
                        }
                    });
                }
            };
            const $container = getContainer(handlers);

            $container.find('.SBSE_SelFilter').hide(); // hide filter selector
            $container.find('.SBSE_BtnExport').remove(); // remove export button
            $container.find('#SBSE_BtnSettings').before(`
                <label><input type="checkbox" class="SBSE_ChkMarketListings">${i18n.get('checkboxMarketListings')}</label>
            `); // append checkbox for market keys

            $('#TableKeys').eq(0).before($container);

            // rate all positive
            const $awaitRatings = $('a[href^="account_page_0_ratepositive"]');

            if ($awaitRatings.length > 0) {
                $('#TableKeys td:contains(Rate TRADE)').text(i18n.get('DIGRateAllPositive')).css('cursor', 'pointer').click(() => {
                    $awaitRatings.each((() => {
                        var _ref6 = _asyncToGenerator(function* (i, a) {
                            const res = yield fetch(a.href, {
                                method: 'GET',
                                credentials: 'same-origin'
                            });

                            if (res.ok) $(a).parent('td').html('<span class="DIG3_14_Orange">Positive</span>');
                        });

                        return function (_x8, _x9) {
                            return _ref6.apply(this, arguments);
                        };
                    })());
                });
            }
            // DIG EasyBuy
        } else if (pathname.includes('/account_digstore') || pathname.includes('/account_trades') || pathname.includes('/account_tradesXT') || pathname.includes('/store_update') || pathname.includes('/storeXT_update')) {
            // inject css styles
            GM_addStyle(`
                .DIGEasyBuy_row { height: 30px; }
                .DIGEasyBuy button { padding: 4px 8px; outline: none; cursor: pointer; }
                .DIGEasyBuy_checked { background-color: #222; }
                .DIGEasyBuy_hideOwned tr.SBSE_hide { display: none; }
                .DIGEasyBuy_hideOwned tr.SBSE_hide + .SBSE_searchResults { display: none; }
                .SBSE_searchResults td { padding: 0 }
                .SBSE_searchResults iframe {
                    width: 100%; height: 300px;
                    display: none;
                    background-color: white;
                    border: none;
                }
                .SBSE_owned a[href*="steam"] .DIG3_14_Gray { color: #9ccc65; }
                .SBSE_wished a[href*="steam"] .DIG3_14_Gray { color: #29b6f6; }
                .SBSE_ignored a[href*="steam"] .DIG3_14_Gray { text-decoration: line-through; }
                .DIG2content select { max-width: 200px; }
            `);

            // setup row data & event
            const easyBuySetup = (i, ele) => {
                const $game = $(ele);
                const $row = $game.closest('tr');

                $row.attr('data-id', $game.attr('href').replace(/\D/g, ''));
                $row.attr('data-title', $row.children('td').eq(pathname.includes('/account_digstore') ? 3 : 1).text().trim());
                $row.attr('data-price', () => {
                    let price = 0;
                    const $DIGPoints = $row.find('td:contains( DIG Points)');

                    if ($DIGPoints.length === 1) price = $DIGPoints.text();else {
                        const tds = $row.children('td').get();

                        for (let j = tds.length - 1; j >= 0; j -= 1) {
                            const text = tds[j].textContent.trim();

                            if (text.startsWith('$')) {
                                price = text.replace(/\D/g, '');
                                break;
                            }
                        }
                    }

                    return parseInt(price, 10);
                });
                $row.click(() => {
                    $row.toggleClass('DIGEasyBuy_checked');
                });
                $row.addClass('DIGEasyBuy_row');
            };

            $('a[href^="account_buy"]').each(easyBuySetup);

            // check if owned / manually hid
            const check = (i, a) => {
                const $a = $(a);
                const $tr = $a.closest('tr');
                const data = a.pathname.slice(1).split('/');
                const steamID = parseInt(data[1], 10);
                const id = parseInt($tr.attr('data-id'), 10);
                const d = { [data[0]]: steamID };

                if (steam.isOwned(d)) $tr.addClass('SBSE_owned SBSE_hide');
                if (steam.isWished(d)) $tr.addClass('SBSE_wished');
                if (steam.isIgnored(d)) $tr.addClass('SBSE_ignored');
                if (MPHideList.includes(id)) $tr.addClass('SBSE_hide');

                // append manual hide feature
                $tr.children().eq(0).attr('title', i18n.get('DIGClickToHideThisRow')).click(e => {
                    e.stopPropagation();

                    if (id > 0) {
                        MPHideList.push(id);
                        GM_setValue('SBSE_DIGMPHideList', JSON.stringify(MPHideList));

                        $tr.addClass('SBSE_hide');
                    }
                });

                // no appID found, pre-load Google search result
                if (steamID === -1 && !MPHideList.includes(id)) {
                    const $game = $a.find('span');
                    const gameTitle = encodeURIComponent($game.text().trim()).replace(/%20/g, '+');
                    const map = {
                        '&': '&amp;',
                        '<': '&lt;',
                        '>': '&gt;',
                        '"': '&quot;',
                        "'": '&#039;'
                    };

                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: `https://www.google.com/search?q=steam+${gameTitle}`,
                        onload: res => {
                            let html = res.responseText;

                            // inset style
                            const index = html.indexOf('</head>');
                            const style = `
                                <style>
                                    body { overflow-x: hidden; }
                                    .sfbgx, #sfcnt, #searchform, #top_nav, #appbar, #taw { display: none; }
                                    #center_col { margin-left: 0 !important; }
                                </style>
                            `;
                            html = html.slice(0, index) + style + html.slice(index);

                            // stripe script tags
                            html = html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');

                            // manipulate urls
                            html = html.replace(/\/images\//g, 'https://www.google.com/images/').replace(/\/url\?/g, 'https://www.google.com/url?');

                            $tr.after(`
                                <tr class="SBSE_searchResults">
                                    <td colspan="11"><iframe sandbox="allow-scripts" srcdoc='${html.replace(/[&<>"']/g, m => map[m])}'></frame></td>
                                </tr>
                            `);
                        }
                    });

                    $game.unwrap('a').css({
                        cursor: 'pointer',
                        color: 'red'
                    }).click(e => {
                        e.stopPropagation();

                        $tr.next('.SBSE_searchResults').find('iframe').slideToggle('fast');
                    });
                }
            };

            $('tr a[href*="steampowered"]').each(check);

            // append menu buttons
            const $target = $('#form3').closest('tr').children().eq(0);
            const $DIGEasyBuy = $(`
                <div class="DIGEasyBuy">
                    <button class="DIGButtonPurchase DIG3_Orange_15_Form">${i18n.get('DIGEasyBuyPurchase')}</button>
                    <button class="DIGButtonSelectAll DIG3_Orange_15_Form">${i18n.get('DIGEasyBuySelectAll')}</button>
                    <button class="DIGButtonHideOwned DIG3_Orange_15_Form">${i18n.get('DIGEasyBuyHideOwned')}</button>
                </div>
            `);

            if ($target.children().length > 0) {
                const $tr = $('<tr/>');

                $tr.append($target.clone());
                $target.parent().before($tr);
            }

            $target.empty().append($DIGEasyBuy);

            if (pathname === '/account_tradesXT.html') {
                $DIGEasyBuy.append(`
                    <button class="DIGButtonLoadAllPages DIG3_Orange_15_Form">${i18n.get('DIGEasyBuyLoadAllPages')}</button>
                `);
            }

            // append sync time and event
            const seconds = Math.round((Date.now() - steam.lastSync('library')) / 1000);

            $DIGEasyBuy.append(`
                <span> ${i18n.get('lastSyncTime').replace('%seconds%', seconds)}</span>
            `);

            // bind button event
            $('.DIGButtonPurchase').click(() => {
                let balance = parseInt($('div:contains(Usable DIG Points) > span').text().split(' ').shift().replace(/\D/g, ''), 10);
                const $games = $('.DIGEasyBuy_checked:visible');

                swal({
                    title: i18n.get('DIGButtonPurchasing'),
                    html: '<p></p>',
                    onOpen: () => {
                        swal.showLoading();
                    }
                });

                (() => {
                    var _ref7 = _asyncToGenerator(function* () {
                        const game = $games.shift();

                        if (game) {
                            const $game = $(game);
                            const id = $game.data('id');
                            const price = parseInt($game.data('price'), 10);
                            const title = $game.data('title');

                            if (title.length > 0) swal.getContent().querySelector('p').textContent = title;

                            if (id && price > 0) {
                                if (balance - price > 0) {
                                    let url = `${location.origin}/account_buy.html`;
                                    const requestInit = {
                                        method: 'POST',
                                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                                        body: `quantity=1&xgameid=${id}&xgameprice1=${price}&send=Purchase`,
                                        mode: 'same-origin',
                                        credentials: 'same-origin',
                                        cache: 'no-store',
                                        referrer: `${location.origin}/account_buy_${id}.html`
                                    };

                                    if (pathname === '/account_trades.html' || pathname === '/account_tradesXT.html') {
                                        url = `${location.origin}/account_buytrade_${id}.html`;
                                        requestInit.body = `gameid=${id}&send=Purchase`;
                                        requestInit.referrer = url;
                                    }

                                    const res = yield fetch(url, requestInit);

                                    if (res.ok) {
                                        $game.click();
                                        balance -= price;
                                    }

                                    purchaseHandler();
                                } else {
                                    swal({
                                        title: i18n.get('failTitle'),
                                        text: i18n.get('DIGInsufficientFund'),
                                        type: 'error'
                                    });
                                }
                            } else purchaseHandler();
                        } else {
                            swal({
                                title: i18n.get('successTitle'),
                                text: i18n.get('DIGFinishedPurchasing'),
                                type: 'success'
                            });
                        }
                    });

                    function purchaseHandler() {
                        return _ref7.apply(this, arguments);
                    }

                    return purchaseHandler;
                })()();
            });
            $('.DIGButtonSelectAll').click(e => {
                const $self = $(e.currentTarget);
                const state = !$self.data('state');
                const selector = $('.DIGEasyBuy_hideOwned').length > 0 ? '.DIGEasyBuy_row:not(.SBSE_hide)' : '.DIGEasyBuy_row';

                $(selector).toggleClass('DIGEasyBuy_checked', state);
                $self.data('state', state);
                $self.text(state ? i18n.get('DIGEasyBuySelectCancel') : i18n.get('DIGEasyBuySelectAll'));
            });
            $('.DIGButtonHideOwned').click(e => {
                const $self = $(e.currentTarget);
                const state = !$self.data('state');

                $('#TableKeys').toggleClass('DIGEasyBuy_hideOwned', state);
                $self.data('state', state);
                $self.text(state ? i18n.get('DIGEasyBuyShowOwned') : i18n.get('DIGEasyBuyHideOwned'));
            });
            $('.DIGButtonLoadAllPages').click(e => {
                // auto load all pages at marketplace
                const $self = $(e.currentTarget);
                const $tbody = $('#TableKeys > tbody');
                const load = (() => {
                    var _ref8 = _asyncToGenerator(function* (page, retry = 0) {
                        $self.text(i18n.get('DIGEasyBuyLoading').replace('%page%', page));

                        const res = yield fetch(`${location.origin}/account_tradesXT_${page}.html`, {
                            method: 'GET',
                            credentials: 'same-origin'
                        });

                        if (res.ok) {
                            const $result = $((yield res.text())).find('#TableKeys tr.DIG3_14_Gray');

                            if ($result.length > 0) {
                                $result.find('a[href^="account_buy"]').each(easyBuySetup);
                                $result.find('a[href*="steampowered"]').each(check);
                                $tbody.append($result);
                                load(page + 1);
                            } else $self.text(i18n.get('DIGEasyBuyLoadingComplete'));
                        } else if (retry < 3) load(page, retry + 1);else load(page + 1);
                    });

                    return function load(_x10) {
                        return _ref8.apply(this, arguments);
                    };
                })();

                load(1);
                $self.prop('disabled', true);
            });
            // extension for creating trade at market place
        } else if (pathname === '/account_createtrade.html') {
            const $form = $('#form_createtrade');

            // create trade page
            if ($form.length > 0) {
                // trim input field
                const $gameTitle = $form.find('input[name="typeahead"]');
                const $steamKey = $form.find('input[name="STEAMkey"]');

                $gameTitle.blur(() => {
                    unsafeWindow.jQuery('input.typeahead').typeahead('setQuery', $gameTitle.val().trim());
                });
                $steamKey.blur(e => {
                    const $self = $(e.delegateTarget);
                    const key = $self.val().match(regKey);

                    if (key) $self.val(key[0]);
                });
                $steamKey.attr({
                    size: 50,
                    maxlength: 200
                });

                // search for current market price when click dropdown menu
                const $searchResult = $('<div/>');

                $gameTitle.closest('table').after($searchResult);
                $searchResult.before(`<h3>${i18n.get('DIGMarketSearchResult')}</h3>`);

                $('.tt-dropdown-menu').click(_asyncToGenerator(function* () {
                    $searchResult.empty();

                    const res = yield fetch(`${location.origin}/account_tradesXT.html`, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                        body: `search=${encodeURIComponent($gameTitle.val()).replace(/%20/g, '+')}&button=SEARCH`,
                        credentials: 'same-origin'
                    });
                    const result = res.ok ? $((yield res.text())).find('#TableKeys') : 'Network response was not ok.';

                    $searchResult.append(result);
                }));

                // apply last input price
                const lastPrice = GM_getValue('SBSE_DIGLastPrice', 20);
                const $priceField = $('input[name=price]');

                $priceField.val(lastPrice).trigger('input');
                $('#form_createtrade').submit(() => {
                    const price = parseInt($priceField.val(), 10);

                    if (price !== lastPrice) GM_setValue('SBSE_DIGLastPrice', price);
                });
                // result page
            } else {
                GM_addStyle(`
                    .check.icon {
                        width: 42px; height: 24px;
                        margin: 12px 0 5px 9px;
                        border-bottom: solid 3px currentColor;
                        border-left: solid 3px currentColor;
                        transform: rotate(-45deg);
                        color: #5cb85c;
                    }
                    .remove.icon { color: #d9534f; margin-left: 9px; margin-top: 30px; }
                    .remove.icon:before, .remove.icon:after {
                        width: 45px; height: 3px;
                        position: absolute;
                        content: '';
                        background-color: currentColor;
                        transform: rotate(45deg);
                    }
                    .remove.icon:after { transform: rotate(-45deg); }
                `);

                const $anchor = $('td.DIG3_14_Gray > table:first-child');
                const IsSucceed = !!$('td.DIG3_14_Gray:contains("The game key has been added to the DIG MarketPlace.")').length;

                if (IsSucceed) $anchor.after('<div class="check icon"></div>');else $anchor.after('<div class="remove icon"></div>');
            }
        }
    },
    ccyycn() {
        // inject css
        GM_addStyle(`
            .SBSE_container {
                width: 80%;
                margin: 0 auto;
                font-size: 16px;
                color: #000;
            }
            .SBSE_container > textarea {
                background-color: #EEE;
                box-shadow: 0 0 1px 1px rgba(204,204,204,0.5);
                border-radius: 5px;
            }
            .SBSE_container > div { text-align: left; }
            .SBSE_container > div button {
                width: 80px;
                border: 1px solid #2e6da4;
                border-radius: 5px;
                background-color: #337ab7;
                color: #FFF;
            }
            .SBSE_container label { color: #EEE; }
            .expanded .showOrderMeta {
                display: block !important;
                position: absolute;
                margin-top: -8px;
                right: 265px;
                z-index: 1;
            }
        `);

        const handlers = {
            extract() {
                const data = {
                    title: 'CCYYCN Bundle',
                    filename: 'CCYYCN Bundle',
                    items: []
                };

                $('.deliver-gkey > *:contains(-)').each((i, ele) => {
                    const $ele = $(ele);
                    const d = {
                        title: $ele.parent().prev().text().trim(),
                        key: $ele.text().trim()
                    };

                    activator.pushKeyDetails(d);
                    data.items.push(d);
                });

                return data;
            },
            reveal(e) {
                const $revealBtn = $(e.currentTarget);
                const handler = ($games, callback) => {
                    const game = $games.shift();

                    if (game) {
                        game.click();
                        setTimeout(handler.bind(null, $games, callback), 300);
                    } else callback();
                };

                $revealBtn.addClass('working');

                handler($('.deliver-btn'), () => {
                    $revealBtn.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                });
            }
        };
        const $container = getContainer(handlers);

        $container.find('.SBSE_SelFilter').hide(); // hide filter selector
        $container.find('button').addClass('narrow'); // narrow buttons

        // insert textarea
        $('.featurette-divider').eq(0).after($container);
    },
    groupees() {
        if (location.pathname.startsWith('/profile/')) {
            // inject css
            GM_addStyle(`
                .SBSE_container > textarea, .SBSE_container > div button {
                    background: transparent;
                    border: 1px solid #8cc53f;
                    border-radius: 3px;
                    color: #8cc53f;
                    transition: all 0.8s ease;
                }
                .SBSE_container > div button:hover {
                    background-color: #8cc53f;
                    color: white;
                    text-decoration: none;
                }
                img.product-cover { display: none; }
            `);
            const handlers = {
                extract() {
                    const bundleTitle = $('h2').text().trim();
                    const data = {
                        title: bundleTitle,
                        filename: `Groupees ${bundleTitle} Keys`,
                        items: []
                    };

                    $('.key-block input.code').each((i, ele) => {
                        const $ele = $(ele);
                        const key = $ele.val();

                        if (key.includes('-')) {
                            const $titleEle = $ele.closest('tr').prev().find('td:nth-of-type(3)');
                            const d = {
                                title: $titleEle.text().trim(),
                                key,
                                used: !!$ele.closest('.key-block').find('.key-status:contains(used)').length
                            };

                            activator.pushKeyDetails(d);
                            data.items.push(d);
                        }
                    });

                    return data;
                },
                reveal(e) {
                    const $revealBtn = $(e.currentTarget);
                    const handler = ($games, callback) => {
                        const game = $games.shift();

                        if (game) {
                            game.click();
                            setTimeout(handler.bind(null, $games, callback), 300);
                        } else callback();
                    };

                    $revealBtn.addClass('working');

                    const $reveals = $('.product:has(img[title*=Steam]) .reveal-product');
                    const timer = $reveals.length > 0 ? 1500 : 0;

                    $reveals.click();
                    setTimeout(() => {
                        handler($('.btn-reveal-key'), () => {
                            $revealBtn.removeClass('working');
                            $('.SBSE_BtnRetrieve').click();
                        });
                    }, timer);
                }
            };
            const $container = getContainer(handlers);

            $container.find('.SBSE_SelFilter').hide(); // hide filter selector

            // append checkbox for used-key
            $('#SBSE_BtnSettings').before(`
                <label><input type="checkbox" class="SBSE_ChkSkipUsed" checked>${i18n.get('checkboxSkipUsed')}</label>
            `);

            // insert container
            $('.table-products').before($container);

            // load details
            $('img[src*="steam.svg"]').each((() => {
                var _ref10 = _asyncToGenerator(function* (index, ele) {
                    $.ajax({
                        url: $(ele).closest('tr').find('.item-link').attr('href'),
                        data: { v: 1 },
                        dataType: 'script'
                    });
                });

                return function (_x11, _x12) {
                    return _ref10.apply(this, arguments);
                };
            })());

            // bind custom event
            $(document).on('activated', (e, key, result) => {
                if (result.success === 1) $(`.btn-steam-redeem[href*=${key}]`).next('.key-usage-toggler').click();
            });
        } else {
            // inject css
            GM_addStyle(`
                .SBSE_container { margin-bottom: 20px; }
                .SBSE_container > textarea { background-color: #EEE; border-radius: 3px; }
                .SBSE_container > div button { outline: none !important; }
                #SBSE_BtnSettings { margin-top: 8px; }
            `);

            const handlers = {
                extract() {
                    const bundleTitle = $('.expanded .caption').text().trim();
                    const data = {
                        title: bundleTitle,
                        filename: `Groupees ${bundleTitle} Keys`,
                        items: []
                    };

                    $('.expanded .code').each((i, ele) => {
                        const $ele = $(ele);
                        const d = {
                            title: $ele.closest('.details').find('h3').text().trim(),
                            key: $ele.val(),
                            used: $ele.closest('li').find('.usage').prop('checked')
                        };

                        activator.pushKeyDetails(d);
                        data.items.push(d);
                    });

                    return data;
                },
                reveal(e) {
                    const $revealBtn = $(e.currentTarget);
                    const handler = ($games, callback) => {
                        const game = $games.shift();

                        if (game) {
                            game.click();
                            setTimeout(handler.bind(null, $games, callback), 300);
                        } else callback();
                    };

                    $revealBtn.addClass('working');

                    const $reveals = $('.product:has(img[title*=Steam]) .reveal-product');
                    const timer = $reveals.length > 0 ? 1500 : 0;

                    $reveals.click();
                    setTimeout(() => {
                        handler($('.expanded .reveal'), () => {
                            $revealBtn.removeClass('working');
                            $('.SBSE_BtnRetrieve').click();
                        });
                    }, timer);
                }
            };
            const $container = getContainer(handlers);

            // append checkbox for used-key
            $container.find('#SBSE_BtnSettings').before($(`
                <label><input type="checkbox" class="SBSE_ChkSkipUsed" checked>${i18n.get('checkboxSkipUsed')}</label>
            `));
            // add buttons style via groupees's class
            $container.find('.SBSE_container > div > button, .SBSE_container > div > a').addClass('btn btn-default');

            // insert container
            $('.container > div').eq(1).before($container);

            // append mark all as used button
            new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach(addedNode => {
                        const $orderMeta = $(addedNode).find('.order-meta');

                        if ($orderMeta.length > 0) {
                            $orderMeta.after($(`<button class="btn btn-default" style="margin-right: 10px;"><b>${i18n.get('markAllAsUsed')}</b></button>`).click(() => {
                                $('.expanded .usage').each((i, checkbox) => {
                                    if (!checkbox.checked) checkbox.click();
                                });
                            }));
                            $orderMeta.parent().addClass('showOrderMeta');
                        }
                    });
                });
            }).observe($('#profile_content')[0], { childList: true });

            // bind custom event
            $(document).on('activated', (e, key, result) => {
                if (result.success === 1) $(`li.key:has(input[value=${key}]) .usage`).click();
            });
        }
    },
    agiso() {
        const keys = unique($('body').text().match(regKey));

        if (keys.length > 0) {
            // inject css
            GM_addStyle(`
                .SBSE_container > textarea { border: 1px solid #AAAAAA; }
                .SBSE_container > div button {
                    border: 1px solid #d3d3d3;
                    background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;
                    color: #555555;
                }
                .SBSE_container > div button:hover {
                    border-color: #999999;
                    background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;
                    color: #212121;
                }
            `);

            const handlers = {
                extract() {
                    const bundleTitle = $('a[href*="tradeSnap.htm"]').eq(1).text().trim();
                    const data = {
                        title: bundleTitle,
                        filename: `agiso ${bundleTitle} Keys`,
                        items: []
                    };

                    keys.forEach(key => {
                        data.items.push({ key });
                    });

                    return data;
                }
            };
            const $container = getContainer(handlers);

            $container.find('.SBSE_SelFilter').hide(); // hide filter selector
            $container.find('.SBSE_BtnReveal').remove(); // remove reveal

            // insert container
            $('#tabs').eq(0).prepend($container);
        }
    },
    steamcn() {
        if (location.pathname.startsWith('/tooltip')) {
            GM_addStyle('body { overflow: hidden; }');
        }
    },
    yuplay() {
        // inject css
        GM_addStyle(`
            .SBSE_container { margin-top: 20px; }
            .SBSE_container > textarea { background-color: rgb(230, 230, 229); color: rgb(27, 26, 26); }
            .SBSE_container > div { text-align: left; }
            .SBSE_container > div button {
                width: 80px;
                border: 1px solid #b4de0a;
                background-color: #b4de0a;
                color: #1a1a1a;
            }
            .SBSE_container > div button:hover {
                border: 1px solid #a4ca09;
                background-color: #a4ca09;
            }
            .SBSE_container label { color: #1a1a1a; font-weight: 400; }
            .SBSE_appList { margin-bottom: 10px; }
            .SBSE_appList td { vertical-align: top; }
            .SBSE_appList a { display: block; margin-bottom: 5px; }
            .SBSE_icon { position: relative; top: 5px; }
        `);

        const handlers = {
            extract() {
                const data = {
                    title: 'Yuplay Games',
                    filename: 'Yuplay Games',
                    items: []
                };

                $('.product-info').each((i, ele) => {
                    const $ele = $(ele);
                    const d = {
                        title: $ele.find('.name').text().trim(),
                        key: $ele.next('.keys').find('input').val()
                    };

                    activator.pushKeyDetails(d);
                    data.items.push(d);
                });

                return data;
            }
        };
        const appListHandler = data => {
            if (data.length > 0) {
                const $appList = $('<table class="SBSE_appList"></table>');

                $appList.append('<tr><td colspan="2">App List</td></tr>');

                data.forEach(d => {
                    const $row = $('<tr/>');

                    $row.append($('<td/>').append($('<span class="SBSE_icon"></span>').mouseenter(steamCNTooltip.show.bind(steamCNTooltip))), $(`<td><a href="https://store.steampowered.com/app/${d.app}" target="_blank">${d.title}</a></td>`));

                    d.owned = steam.isOwned(d);
                    d.wished = steam.isWished(d);

                    if (d.owned) $row.addClass('SBSE_owned');
                    if (d.wished) $row.addClass('SBSE_wished');

                    $row.addClass('SBSE_processed isSteam').attr('data-gameinfo', JSON.stringify(d));

                    $appList.append($row);
                });

                $('.list-character').after($appList);

                // load SteamCN tooltip
                steamCNTooltip.load(data);
            }
        };
        const $container = getContainer(handlers);

        $container.find('.SBSE_SelFilter').hide(); // hide filter selector
        $container.find('button').addClass('narrow'); // narrow buttons
        $container.find('.SBSE_BtnReveal').remove(); // remove reveal

        // insert textarea
        $('.table-light').eq(0).before($container);

        // append info from SteamDB if found subid
        $('.list-character p').each((i, ele) => {
            const $ele = $(ele);
            const text = $ele.text().trim();

            if (text.startsWith('Steam')) {
                const subID = text.match(/\d+/)[0];
                const steamDBUrl = `https://steamdb.info/sub/${subID}/`;
                const steamDBKey = `SBSE_steamDB_sub_${subID}`;
                const steamDBData = GM_getValue(steamDBKey, '');

                $ele.find('span').replaceWith(`<a href="${steamDBUrl}" target="_blank">${subID}</a>`);

                if (steamDBData.length === 0) {
                    GM_xmlhttpRequest({
                        url: steamDBUrl,
                        method: 'GET',
                        onload(res) {
                            if (res.status === 200) {
                                const data = [];

                                $(res.response).find('#apps .app').each((j, app) => {
                                    const $app = $(app);
                                    const d = {
                                        title: $app.children('td').eq(2).text().trim(),
                                        app: parseInt($app.attr('data-appid'), 10)
                                    };

                                    data.push(d);
                                });

                                GM_setValue(steamDBKey, JSON.stringify(data));
                                appListHandler(data);
                            }
                        }
                    });
                } else appListHandler(JSON.parse(steamDBData));
            }
        });
    },
    'gama-gama': () => {
        // inject css
        GM_addStyle(`
            .SBSE_container > textarea { background-color: #ededed; color: #33; border-radius: 4px; }
            .SBSE_container > div button {
                width: 80px; height: 35px;
                border: none; border-radius: 4px;
                background: linear-gradient(to bottom, #47bceb 0, #18a4dd 30%, #127ba6 100%);
                color: #fff;
                box-shadow: 0 1px 3px 1px rgba(0,0,0,.8);
            }
            .SBSE_container > div button { font-family: inherit; font-size: inherit; }
            .SBSE_container > div button:hover { background: linear-gradient(to bottom, #47bceb, #18a4dd); }
        `);

        const handlers = {
            extract() {
                const data = {
                    title: 'Gama Gama Games',
                    filename: 'Gama Gama Games',
                    items: []
                };

                $('.gift-line').each((i, ele) => {
                    const $ele = $(ele);

                    $ele.find('.key-list > li').each((j, key) => {
                        const d = {
                            title: $ele.find('.gift-header').text().trim(),
                            key: key.textContent.trim()
                        };

                        activator.pushKeyDetails(d);
                        data.items.push(d);
                    });
                });

                return data;
            }
        };
        const $container = getContainer(handlers);

        $container.find('.SBSE_SelFilter').hide(); // hide filter selector
        $container.find('button').addClass('narrow'); // narrow buttons
        $container.find('.SBSE_BtnReveal').remove(); // remove reveal

        // insert textarea
        $('.user-info').eq(0).after($container);
    },
    plati() {
        const plati = {
            data: JSON.parse(GM_getValue('SBSE_plati', '{}')),
            save(callback) {
                GM_setValue('SBSE_plati', JSON.stringify(this.data));

                if (typeof callback === 'function') callback();
            },
            set(key, value, callback) {
                this.data[key] = value;
                this.save(callback);
            },
            setItem(id, value, save) {
                this.data.itemData[id] = value;
                if (save) this.save();
            },
            get(key) {
                return has.call(this.data, key) ? this.data[key] : null;
            },
            getItem(id) {
                return has.call(this.data.itemData, id) ? this.data.itemData[id] : null;
            },
            init() {
                if (!has.call(this.data, 'enablePlatiFeature')) this.data.enablePlatiFeature = true;
                if (!has.call(this.data, 'fetchOnStart')) this.data.fetchOnStart = true;
                if (!has.call(this.data, 'itemData')) this.data.itemData = {};
                if (!has.call(this.data, 'filterGame')) this.data.filterGame = true;
                if (!has.call(this.data, 'filterDLC')) this.data.filterDLC = true;
                if (!has.call(this.data, 'filterPackage')) this.data.filterPackage = true;
                if (!has.call(this.data, 'filterBundle')) this.data.filterBundle = true;
                if (!has.call(this.data, 'filterOwned')) this.data.filterOwned = true;
                if (!has.call(this.data, 'filterWished')) this.data.filterWished = true;
                if (!has.call(this.data, 'filterIgnored')) this.data.filterIgnored = true;
                if (!has.call(this.data, 'filterNotOwned')) this.data.filterNotOwned = true;
                if (!has.call(this.data, 'filterNotApplicable')) this.data.filterNotApplicable = true;
                if (!has.call(this.data, 'filterNotFetched')) this.data.filterNotFetched = true;

                this.save();
            }
        };

        plati.init();

        if (location.pathname.startsWith('/seller/')) {
            // inject css styles
            GM_addStyle(`
                li[class*="hide"] { display: none; }
                .SBSE_plati_menu { display: flex; margin-bottom: 10px; }
                .SBSE_plati_menu > li { height: 30px; line-height: 30px; padding-right: 50px; }
                .SBSE_plati_menu > li > .SBSE_switch { vertical-align: text-bottom; }
                .SBSE_plati_menu > li > * { cursor: pointer; }
                .SBSE_dropdown_list { width: max-content; z-index: 999; box-shadow: 5px 5px 10px grey; }
                .SBSE_dropdown_list li { cursor: default; }
                .SBSE_dropdown_list li > label, .SBSE_dropdown_list li > span { width: 100%; display: inline-block; margin: 0 10px; cursor: pointer; text-align: left; }
                tr.SBSE_processed:hover { background-color: #f3f3f3; }
                tr.SBSE_processed:hover .product-title > div::after { display: none; }
                .filterGame tr.SBSE_game,
                .filterDLC tr.SBSE_DLC,
                .filterPackage tr.SBSE_package,
                .filterBundle tr.SBSE_bundle,
                .filterOwned tr.SBSE_owned,
                .filterWished tr.SBSE_wished,
                .filterIgnored tr.SBSE_ignored,
                .filterNotOwned tr.SBSE_notOwned,
                .filterNotApplicable tr.SBSE_notApplicable,
                .filterNotFetched tr.SBSE_notFetched { display: none; }
            `);
            // preparing
            let selectedCurrency = GM_getValue('SBSE_selectedCurrency', 'USD');
            let platiCurrency = $('th.product-price select option:selected').text().trim();
            const language = config.get('language');
            const fetchItem = (() => {
                var _ref11 = _asyncToGenerator(function* (queue) {
                    const tr = queue.shift();

                    if (tr) {
                        const $tr = $(tr);
                        const url = $tr.attr('data-url');
                        const id = parseInt($tr.attr('data-id'), 10);
                        const classes = ['SBSE_fetching'];

                        if (url.length > 0 && id > 0) {
                            const res = yield fetch(url);

                            if (res.ok) {
                                const itemPageHTML = yield res.text();
                                const description = itemPageHTML.slice(itemPageHTML.indexOf('goods-descr-text'), itemPageHTML.indexOf('goods_reviews'));
                                const found = description.match(regURL);

                                if (found) {
                                    const type = found[3].slice(0, 3).toLowerCase();
                                    const steamID = parseInt(found[4], 10);
                                    const item = {};
                                    item[type] = steamID;

                                    plati.setItem(id, item);
                                    if (steam.isOwned(item)) classes.push('SBSE_owned');
                                    if (steam.isWished(item)) classes.push('SBSE_wished');
                                    if (steam.isIgnored(item)) classes.push('SBSE_ignored');
                                    if (classes.length === 1) classes.push('SBSE_notOwned');
                                    if (steam.isGame(item)) classes.push('SBSE_game');
                                    if (steam.isDLC(item)) classes.push('SBSE_DLC');
                                    if (steam.isPackage(item)) classes.push('SBSE_package');
                                } else {
                                    plati.setItem(id, {});
                                    classes.push('SBSE_notApplicable');
                                }
                            } else classes.push('SBSE_failed');
                        }

                        $tr.removeClass('SBSE_owned SBSE_wished SBSE_ignored SBSE_notOwned SBSE_notApplicable');
                        $tr.toggleClass(classes.join(' '));
                        fetchItem(queue);
                    } else plati.save();
                });

                return function fetchItem(_x13) {
                    return _ref11.apply(this, arguments);
                };
            })();
            const fetchItems = items => {
                const filter = plati.get('fetchOnStart') ? ':not(.SBSE_fetched)' : '';
                const $trs = items && items.length > 0 ? $(items).filter(filter) : $(`.goods-table tbody > tr${filter}`);
                const queue = $trs.get();

                $trs.addClass('SBSE_fetching').removeClass('SBSE_notFetched');
                fetchItem(queue);
            };
            const process = table => {
                if (plati.get('enablePlatiFeature')) {
                    const $table = table ? $(table) : $('.goods-table');
                    platiCurrency = $table.find('th.product-price select option:selected').text().trim();

                    $table.addClass('showType showIcon SBSE_icon-vMiddle').find('thead tr:not(:has(.status)) > .product-sold').before('<th class="type"></th><th class="status"></th>');

                    $table.find('tbody > tr:not(.SBSE_processed)').each((i, tr) => {
                        const $tr = $(tr);
                        const url = $tr.find('.product-title a').attr('href');
                        const id = parseInt(url.split('/').pop(), 10);

                        if (url.length > 0 && id > 0) {
                            const classes = [];
                            const item = plati.getItem(id);

                            if (item !== null) {
                                classes.push('SBSE_fetched');
                                if (item.app || item.sub) {
                                    if (steam.isOwned(item)) classes.push('SBSE_owned');
                                    if (steam.isWished(item)) classes.push('SBSE_wished');
                                    if (steam.isIgnored(item)) classes.push('SBSE_ignored');
                                    if (classes.length === 1) classes.push('SBSE_notOwned');
                                    if (steam.isGame(item)) classes.push('SBSE_game');
                                    if (steam.isDLC(item)) classes.push('SBSE_DLC');
                                    if (steam.isPackage(item)) classes.push('SBSE_package');
                                } else classes.push('SBSE_notApplicable');

                                $tr.attr('data-item', JSON.stringify(item));
                            }

                            if (classes.length > 0) $tr.addClass(classes.join(' '));else $tr.addClass('SBSE_notFetched');

                            $tr.attr({
                                'data-id': id,
                                'data-url': location.origin + url
                            });
                        }

                        // setup price node
                        const $price = $tr.find('.product-price div');
                        const value = parseFloat($price.text().trim()) * 100;

                        $price.replaceWith(`<span class="SBSE_price" data-currency="${platiCurrency}" data-value="${value}"></span>`);
                    }).addClass('SBSE_processed').find('.product-sold').before('<td><span class="SBSE_type"></span></td><td><span class="SBSE_icon"></span></td>');

                    // auto fetch on page visit
                    if (plati.get('fetchOnStart')) fetchItems($table.find('tbody > tr'));

                    xe.update(selectedCurrency);
                }
            };

            // set up menu
            const $menu = $(`
                <ul class="SBSE_plati_menu">
                    <li data-config="enablePlatiFeature">
                        <label class="SBSE_switch SBSE_switch-small">
                            <input type="checkbox" id="enablePlatiFeature">
                            <span class="SBSE_slider"></span>
                        </label>
                        <label for="enablePlatiFeature"><span>${i18n.get('enablePlatiFeature')}</span></label>
                    </li>
                    <li data-config="fetchOnStart">
                        <label class="SBSE_switch SBSE_switch-small">
                            <input type="checkbox" id="fetchOnStart">
                            <span class="SBSE_slider"></span>
                        </label>
                        <label for="fetchOnStart"><span>${i18n.get('platiFetchOnStart')}</span></label>
                    </li>
                    <li data-config="fetchButton"><span>${i18n.get('platiFetchButton')}</span></li>
                    <li data-config="filterType" class="SBSE_dropdown">
                        <span>${i18n.get('platiFilterType')}</span>
                        <ul class="SBSE_dropdown_list">
                            <li><label><input type="checkbox" data-filter="filterGame"><span>${i18n.get('game')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterDLC"><span>${i18n.get('dlc')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterPackage"><span>${i18n.get('package')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterBundle"><span>${i18n.get('bundle')}</span></label></li>
                        </ul>
                    </li>
                    <li data-config="filterStatus" class="SBSE_dropdown">
                        <span>${i18n.get('platiFilterStatus')}</span>
                        <ul class="SBSE_dropdown_list">
                            <li><label><input type="checkbox" data-filter="filterOwned"><span>${i18n.get('owned')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterWished"><span>${i18n.get('wished')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterIgnored"><span>${i18n.get('ignored')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterNotOwned"><span>${i18n.get('notOwned')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterNotApplicable"><span>${i18n.get('notApplicable')}</span></label></li>
                            <li><label><input type="checkbox" data-filter="filterNotFetched"><span>${i18n.get('notFetched')}</span></label></li>
                        </ul>
                    </li>
                    <li data-config="currency" class="SBSE_dropdown">
                        <span class="selectedCurrency">${xe.currencies[selectedCurrency][language]}</span>
                        <ul class="SBSE_dropdown_list"></ul>
                    </li>
                </ul>
            `);
            const $enablePlatiFeature = $menu.find('[data-config="enablePlatiFeature"] input');
            const $fetchOnStart = $menu.find('[data-config="fetchOnStart"] input');
            const $fetchButton = $menu.find('[data-config="fetchButton"] span');
            const $filters = $menu.find('[data-config="filterType"] input, [data-config="filterStatus"] input');
            const $currencyToggler = $menu.find('[data-config="currency"] ul');

            $enablePlatiFeature.change(() => {
                const state = $enablePlatiFeature.prop('checked');

                plati.set('enablePlatiFeature', state);
                $menu.find('li:not([data-config="enablePlatiFeature"])').toggleClass('hide1', !state);

                if (state) process();
            });
            $fetchOnStart.change(() => {
                const state = $fetchOnStart.prop('checked');

                plati.set('fetchOnStart', state);
                $fetchButton.parent().toggleClass('hide2', state);
            });
            $fetchButton.click(fetchItems);
            $filters.change(e => {
                const input = e.delegateTarget;
                const filter = input.dataset.filter;
                const state = input.checked;

                plati.set(filter, state);
                $('.goods-table').toggleClass(filter, !state);
            });
            Object.keys(xe.currencies).forEach(currency => {
                const currencyName = xe.currencies[currency][language];

                $currencyToggler.append($(`<span>${currencyName}</span>`).click(() => {
                    xe.update(currency);
                    selectedCurrency = currency;
                    $currencyToggler.prev('.selectedCurrency').text(currencyName);
                }));
            });
            $currencyToggler.find('span').wrap('<li></li>');

            // apply config
            $enablePlatiFeature.prop('checked', plati.get('enablePlatiFeature'));
            $menu.find('li:not([data-config="enablePlatiFeature"])').toggleClass('hide1', !plati.get('enablePlatiFeature'));
            $fetchOnStart.prop('checked', plati.get('fetchOnStart'));
            $fetchButton.parent().toggleClass('hide2', plati.get('fetchOnStart'));
            $filters.each((i, input) => {
                const filter = input.dataset.filter;
                const state = plati.get(filter);

                input.checked = state;
                $('.goods-table').toggleClass(filter, !state);
            });

            $('.merchant_products').prepend($menu);

            process();

            // detect list changes
            new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach(addedNode => {
                        const $addedNode = $(addedNode);

                        if ($addedNode.is('table.goods-table')) process($addedNode);
                    });
                });
            }).observe($('#GoodsBlock')[0], {
                childList: true,
                subtree: true
            });
        }
    }
};
const init = () => {
    config.init();
    i18n.init();
    xe.init();
    steam.init();

    if (location.hostname === 'store.steampowered.com') {
        // save sessionID
        if (unsafeWindow.g_AccountID > 0) {
            const currentID = config.get('sessionID');
            const sessionID = unsafeWindow.g_sessionID || '';
            const language = unsafeWindow.g_oSuggestParams.l || 'english';

            if (!config.get('language')) config.set('language', language);
            if (sessionID.length > 0) {
                const update = config.get('autoUpdateSessionID') && currentID !== sessionID;

                if (!currentID || update) {
                    config.set('sessionID', sessionID, () => {
                        swal({
                            title: i18n.get('updateSuccessTitle'),
                            text: i18n.get('updateSuccess'),
                            type: 'success',
                            timer: 3000
                        });
                    });
                }
            }
        }
    } else {
        const site = location.hostname.replace(/(www|alds|bundle|steamdb)\./, '').split('.').shift();

        // check sessionID
        if (!config.get('sessionID')) getSessionID();

        if (has.call(siteHandlers, site)) siteHandlers[site](true);
    }

    steamCNTooltip.listen();
};

$(init);