Steam Bundle Sites Extension

A steam bundle sites' tool kits.

当前为 2018-02-24 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Steam Bundle Sites Extension
// @namespace    http://tampermonkey.net/
// @version      1.14.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      http*://*bundlestars.com/*
// @include      https://www.fanatical.com/*
// @include      https://www.humblebundle.com/downloads*
// @include      https://www.humblebundle.com/home/*
// @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/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.9.2/sweetalert2.min.js
// @resource     sweetalert2CSS https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.9.2/sweetalert2.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
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        unsafeWindow
// @run-at       document-start
// @noframes
// ==/UserScript==

/* global swal */

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

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

// inject swal css
GM_addStyle(GM_getResourceText('sweetalert2CSS'));

// inject script css
GM_addStyle(`
    pre.SBSE_errorMsg { height: 200px; text-align: left; white-space: pre-wrap; }
`);

// load up
const regKey = /(?:(?:([A-Z0-9])(?!\1{4})){5}-){2,5}[A-Z0-9]{5}/g;
const eol = "\r\n";

const has = Object.prototype.hasOwnProperty;
const unique = a => [...new Set(a)];

const steam = JSON.parse(localStorage.getItem('SBSE_steam') || '{}');
const activated = {
    data: JSON.parse(GM_getValue('SBSE_activated') || '[]'),
    push(key) {
        this.data.push(key);
        GM_setValue('SBSE_activated', JSON.stringify(this.data));
    },
    check(key) {
        return this.data.includes(key);
    }
};
const config = {
    data: JSON.parse(GM_getValue('SBSE_config') || '{}'),
    set(key, value, callback = null) {
        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, 'preselectIncludeTitle')) this.data.preselectIncludeTitle = false;
        if (!has.call(this.data, 'titleComesLast')) this.data.titleComesLast = false;
        if (!has.call(this.data, 'preselectJoinKeys')) this.data.preselectJoinKeys = false;
        if (!has.call(this.data, 'joinKeysASFStyle')) this.data.joinKeysASFStyle = true;
        if (!has.call(this.data, 'activateAllKeys')) this.data.activateAllKeys = false;
    }
};
const keyDetails = {
    data: {},
    set(key = '', obj) {
        if (key.length > 0) {
            obj.title = has.call(obj, 'title') ? obj.title.trim() : '';
            if (has.call(obj, 'app')) obj.app = parseInt(obj.app, 10);
            if (has.call(obj, 'sub')) obj.sub = parseInt(obj.sub, 10);
            if (has.call(obj, 'url')) {
                const matched = obj.url.match(/steam.+\/(app|sub)\/(\d+)/);

                if (matched) obj[matched[1]] = parseInt(matched[2], 10);
            }

            this.data[key] = obj;
        }
    },
    get(key) {
        return has.call(this.data, key) ? this.data[key] : null;
    },
    isOwned(key) {
        const detail = this.data[key];

        if (detail && steam.owned.app.includes(detail.app)) return true;
        if (detail && steam.owned.sub.includes(detail.sub)) return true;

        return false;
    }
};
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;
    }
};

config.init();

// text
const i18n = {
    tchinese: {
        name: '繁體中文',
        updateSuccessTitle: '更新成功!',
        updateSuccess: '成功更新Steam sessionID',
        successStatus: '成功',
        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: '語言',
        settingsPreselectIncludeTitle: '預選包括遊戲名',
        settingsTitleComesLast: '遊戲名置後',
        settingsPreselectJoinKeys: '預選合併序號',
        settingsJoinKeysASFStyle: '合併ASF 格式序號',
        settingsActivateAllKeys: '不跳過、啟動所有序號',
        HBAlreadyOwned: '遊戲已擁有',
        HBRedeemAlreadyOwned: '確定刮開 %title% Steam 序號?',
        HBActivationRestrictions: '啟動限制',
        HBDisallowedCountries: '限制以下地區啟動',
        HBExclusiveCountries: '僅限以下地區啟動',
        DIGEasyBuyPurchase: '購買',
        DIGEasyBuySelectAll: '全選',
        DIGEasyBuySelectCancel: '取消',
        DIGEasyBuyHideOwned: '隱藏已擁有',
        DIGEasyBuyShowOwned: '顯示已擁有',
        DIGEasyBuyLoadAllPages: '加載所有頁',
        DIGEasyBuyLoading: '加載第%page%頁中',
        DIGEasyBuyLoadingComplete: '加載完成',
        DIGButtonPurchasing: '購買中',
        DIGInsufficientFund: '餘額不足,準備回到帳號頁',
        DIGMarketSearchResult: '目前市集上架中',
        DIGRateAllPositive: '全部好評',
        DIGClickToHideThisRow: '隱藏此上架遊戲',
        buttonReveal: '刮開',
        buttonRetrieve: '提取',
        buttonActivate: '啟動',
        buttonCopy: '複製',
        buttonReset: '清空',
        buttonExport: '匯出',
        checkboxIncludeGameTitle: '遊戲名',
        checkboxJoinKeys: '合併',
        checkboxSkipUsed: '跳過已使用',
        checkboxSkipOwned: '跳過已擁有',
        checkboxMarketListings: '上架於市集',
        selectConnector: '至',
        markAllAsUsed: '標記全部已使用',
        syncSuccessTitle: '同步成功',
        syncSuccess: '成功同步Steam 遊戲庫資料',
        syncFailTitle: '同步失敗',
        syncFail: '失敗同步Steam 遊戲庫資料',
        visitSteam: '前往Steam',
        lastSyncTime: '已於%seconds% 秒前同步收藏庫'
    },
    schinese: {
        name: '简体中文',
        updateSuccessTitle: '更新成功',
        updateSuccess: '成功更新Steam sessionID',
        successStatus: '成功',
        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: '语言',
        settingsPreselectIncludeTitle: '预选包括游戏名',
        settingsTitleComesLast: '游戏名置后',
        settingsPreselectJoinKeys: '预选合并激活码',
        settingsJoinKeysASFStyle: '合并ASF 格式激活码',
        settingsActivateAllKeys: '不跳过、激活所有激活码',
        HBAlreadyOwned: '游戏已拥有',
        HBRedeemAlreadyOwned: '确定刮开 %title% Steam 激活码?',
        HBActivationRestrictions: '激活限制',
        HBDisallowedCountries: '限制以下地区激活',
        HBExclusiveCountries: '仅限以下地区激活',
        DIGEasyBuyPurchase: '购买',
        DIGEasyBuySelectAll: '全选',
        DIGEasyBuySelectCancel: '取消',
        DIGEasyBuyHideOwned: '隐藏已拥有',
        DIGEasyBuyShowOwned: '显示已拥有',
        DIGEasyBuyLoadAllPages: '加载所有页',
        DIGEasyBuyLoading: '加载第%page%页中',
        DIGEasyBuyLoadingComplete: '加载完成',
        DIGButtonPurchasing: '购买中',
        DIGInsufficientFund: '余额不足,准备回到账号页',
        DIGMarketSearchResult: '目前市集上架中',
        DIGRateAllPositive: '全部好评',
        DIGClickToHideThisRow: '隐藏此上架游戏',
        buttonReveal: '刮开',
        buttonRetrieve: '提取',
        buttonActivate: '激活',
        buttonCopy: '复制',
        buttonReset: '清空',
        buttonExport: '导出',
        checkboxIncludeGameTitle: '游戏名',
        checkboxJoinKeys: '合并',
        checkboxSkipUsed: '跳过已使用',
        checkboxSkipOwned: '跳过已拥有',
        checkboxMarketListings: '上架于市集',
        selectConnector: '至',
        markAllAsUsed: '标记全部已使用',
        syncSuccessTitle: '同步成功',
        syncSuccess: '成功同步Steam 游戏库资料',
        syncFailTitle: '同步失败',
        syncFail: '失败同步Steam 游戏库资料',
        visitSteam: '前往Steam',
        lastSyncTime: '已于%seconds% 秒前同步游戏库'
    },
    english: {
        name: 'English',
        updateSuccessTitle: 'Update Successful!',
        updateSuccess: 'Steam sessionID is successfully updated',
        successStatus: 'Success',
        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',
        settingsPreselectIncludeTitle: 'Pre-select Include Title',
        settingsTitleComesLast: 'Title Comes Last',
        settingsPreselectJoinKeys: 'Pre-select Join Keys',
        settingsJoinKeysASFStyle: 'Join Keys w/ ASF Style',
        settingsActivateAllKeys: 'No skip & activate all keys',
        HBAlreadyOwned: 'Game Already Owned',
        HBRedeemAlreadyOwned: 'Are you sure to redeem %title% Steam Key?',
        HBActivationRestrictions: 'Activation Restrictions',
        HBDisallowedCountries: 'Cannot be activated in the following regions',
        HBExclusiveCountries: 'Can only be activated in the following regions',
        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, returning to account page',
        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',
        checkboxSkipOwned: 'Skip Owned',
        checkboxMarketListings: 'Market Listings',
        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'
    }
};
let text = has.call(i18n, config.get('language')) ? i18n[config.get('language')] : i18n.english;

// inject settings panel css
GM_addStyle(`
    .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 .switch { position: relative; display: inline-block; width: 60px; }
    .SBSE_settings .switch input { display: none; }
    .SBSE_settings .slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        transition: 0.4s;
    }
    .SBSE_settings .slider:before {
        position: absolute;
        content: "";
        height: 26px;
        width: 26px;
        left: 2px;
        bottom: 2px;
        background-color: white;
        transition: 0.4s;
    }
    .SBSE_settings input:checked + .slider { background-color: #2196F3; }
    .SBSE_settings input:focus + .slider { box-shadow: 0 0 1px #2196F3; }
    .SBSE_settings input:checked + .slider:before { transform: translateX(30px); }
    .SBSE_settings > span { display: inline-block; cursor: pointer; color: white; }
`);

// functions
const getSessionID = () => {
    GM_xmlhttpRequest({
        method: 'GET',
        url: 'http://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: text.notLoggedInTitle,
                        text: text.notLoggedInMsg,
                        type: 'error',
                        showCancelButton: true
                    }).then(() => {
                        window.open('http://store.steampowered.com/');
                    });
                }
            }
        }
    });
};
const syncLibrary = (notify = true) => {
    GM_xmlhttpRequest({
        method: 'GET',
        url: `http://store.steampowered.com/dynamicstore/userdata/t=${Math.random()}`,
        onload: res => {
            try {
                const data = JSON.parse(res.response);

                if (data.rgOwnedApps.length === 0 || data.rgOwnedPackages.length === 0) throw Error('Empty library data');

                if (!has.call(steam, 'owned')) steam.owned = {};
                if (!has.call(steam, 'wished')) steam.wished = {};
                if (!has.call(steam, 'ignored')) steam.ignored = {};

                steam.owned.app = data.rgOwnedApps;
                steam.owned.sub = data.rgOwnedPackages;
                steam.wished.app = data.rgWishlist;
                steam.wished.sub = [];
                steam.ignored.app = data.rgIgnoredApps;
                steam.ignored.sub = data.rgIgnoredPackages;
                steam.lastSync = Date.now();

                localStorage.setItem('SBSE_steam', JSON.stringify(steam));

                if (notify) {
                    swal({
                        title: text.syncSuccessTitle,
                        text: text.syncSuccess,
                        type: 'success',
                        timer: 3000
                    });
                }
            } catch (e) {
                swal({
                    title: text.syncFailTitle,
                    text: text.syncFail,
                    type: 'error',
                    confirmButtonText: text.visitSteam,
                    showCancelButton: true
                }).then(result => {
                    if (result.value === true) window.open('http://store.steampowered.com/');
                });
            }
        }
    });
};
const settings = {
    construct() {
        const panelHTML = `
            <div class="SBSE_settings">
                <table>
                    <tr>
                        <td class="name">${text.settingsAutoUpdateSessionID}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="autoUpdateSessionID">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsSessionID}</td>
                        <td class="value">
                            <input type="text" class="sessionID" value="${config.get('sessionID')}">
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsSyncLibrary}</td>
                        <td class="value">
                            <button class="syncLibrary">${text.settingsSyncLibraryButton}</button>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsLanguage}</td>
                        <td class="value">
                            <select class="language"></select>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsPreselectIncludeTitle}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="preselectIncludeTitle">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsTitleComesLast}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="titleComesLast">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsPreselectJoinKeys}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="preselectJoinKeys">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsJoinKeysASFStyle}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="joinKeysASFStyle">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsActivateAllKeys}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="activateAllKeys">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                </table>
            </div>
        `;

        return panelHTML;
    },
    display() {
        swal({
            title: text.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(syncLibrary);

        // language
        Object.keys(i18n).forEach(language => {
            $language.append(new Option(i18n[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);

            text = has.call(i18n, newLanguage) ? i18n[newLanguage] : i18n.english;

            setTimeout(swal.hideLoading, 500);
        });
    }
};
const activateHandler = {
    keys: [],
    results: {},
    updateResults(txt = null) {
        const $textarea = $('.SBSE_container > textarea');

        if (txt) $textarea.val(txt);else {
            const results = this.results;
            const parsed = [];

            Object.values(results).forEach(result => {
                parsed.push(result.join(' | '));
            });

            $textarea.val(parsed.join(eol));
        }
    },
    getResultStatus(result) {
        let status = text.failStatus;
        let statusMsg = text.failDetailUnexpected;
        const errors = {
            14: text.failDetailInvalidKey,
            15: text.failDetailUsedKey,
            53: text.failDetailRateLimited,
            13: text.failDetailCountryRestricted,
            9: text.failDetailAlreadyOwned,
            24: text.failDetailMissingBaseGame,
            36: text.failDetailPS3Required,
            50: text.failDetailGiftWallet
        };

        if (result.success === 1) {
            status = text.successStatus;
            statusMsg = text.successDetail;
        } else if (result.success === 2) {
            if (has.call(errors, result.purchase_result_details)) {
                statusMsg = errors[result.purchase_result_details];
            }
        }

        return `${status}/${statusMsg}`;
    },
    getResultItems(info) {
        const descriptions = [];

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

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

                descriptions.push(description.join(' '));
            });
        }

        return descriptions.join(', ');
    },
    activateKey(callback) {
        const self = this;
        const key = self.keys.shift();

        if (key) {
            if (activated.check(key)) {
                self.results[key].push(`${text.skippedStatus}/${text.activatedDetail}`, text.noItemDetails);
                self.updateResults();

                // next key
                self.activateKey(callback);
            } else if (keyDetails.isOwned(key) && !config.get('activateAllKeys')) {
                const detail = keyDetails.get(key);
                const itemDetail = `${detail.app || detail.sub}, ${detail.title}`;

                self.results[key].push(`${text.skippedStatus}/${text.failDetailAlreadyOwned}`, itemDetail);
                self.updateResults();

                // next key
                self.activateKey(callback);
            } else {
                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) {
                            let status = '';
                            let items = '';

                            try {
                                const result = JSON.parse(res.response);

                                status = self.getResultStatus(result);
                                items = self.getResultItems(result.purchase_receipt_info);

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

                                    // dispatch activated event
                                    $(document).trigger('activated', [key, result]);
                                }
                            } catch (e) {
                                status = `${text.failStatus}/${text.failDetailParsingFailed}`;
                                items = text.noItemDetails;
                            }

                            self.results[key].push(status, items);
                            self.updateResults();

                            // next key
                            setTimeout(self.activateKey.bind(self, callback), 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: text.failTitle,
                                html: text.failDetailRequestFailedNeedUpdate + eol + errorMsg.join(''),
                                type: 'error'
                            });
                            getSessionID();
                            callback();
                        }
                    }
                });
            }
        } else callback();
    },
    activateKeys(input, callback) {
        const self = this;
        const keys = unique(input.match(regKey));

        if (keys.length > 0) {
            keys.forEach(key => {
                self.results[key] = [key];
            });
            self.keys = Object.keys(self.results);
            self.updateResults();
            self.activateKey(callback);
        } else {
            self.updateResults(text.emptyInput);
            callback();
        }
    }
};
const bundleSitesBoxHandler = {
    reveal(handler, $games) {
        const $reveal = $('.SBSE_BtnReveal');

        $reveal.addClass('working');

        handler($games, () => {
            $reveal.removeClass('working');
            $('.SBSE_BtnRetrieve').click();
        });
    },
    retrieve(data) {
        if (data.length > 0) {
            const includeTitle = !!$('.SBSE_ChkTitle:checked').length;
            const joinKeys = !!$('.SBSE_ChkJoin:checked').length;
            const separator = joinKeys ? ',' : eol;
            const prefix = joinKeys && config.get('joinKeysASFStyle') ? '!redeem ' : '';
            const keys = [];

            data.forEach(d => {
                if (typeof d === 'string') {
                    keys.push(d);
                } else {
                    const temp = [d.key];

                    if (includeTitle) temp.unshift(d.title);
                    if (config.get('titleComesLast')) temp.reverse();

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

            $('.SBSE_container > textarea').val(prefix + keys.join(separator));
        }
    },
    activate(e) {
        const $self = $(e.delegateTarget);
        const $textarea = $('.SBSE_container > textarea');
        let input = $textarea.val().trim();

        if (input.length === 0) {
            $('.SBSE_BtnRetrieve').click();
            input = $textarea.val();
        }

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

        activateHandler.activateKeys(input, () => {
            $self.prop('disabled', false).removeClass('working');
            $textarea.removeAttr('disabled');
        });
    },
    copy() {
        $('.SBSE_container > textarea').select();
        document.execCommand('copy');
    },
    reset() {
        $('.SBSE_container > textarea').val('');
    },
    export(data, title) {
        // data: [{key: ..., title: ...}, ...]
        const $export = $('.SBSE_BtnExport');

        $export.removeAttr('href').removeAttr('download');

        if (Array.isArray(data) && data.length > 0) {
            const filename = title.replace(/[\\/:*?"<>|!]/g, '');
            const formattedData = data.map(line => `${line.title.replace(/,/g, ' ')},${line.key}`).join(eol);

            $export.attr('href', `data:text/csv;charset=utf-8,\ufeff${encodeURIComponent(formattedData)}`).attr('download', `${filename}.csv`);
        }
    },
    settings() {
        settings.display();
    }
};
const bundleSitesBox = () => {
    GM_addStyle(`
        .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, .SBSE_container > div > a {
            width: 120px;
            position: relative;
            margin-right: 10px;
            line-height: 28px;
            box-sizing: border-box;
            outline: none;
            cursor: pointer;
            transition: all 0.5s;
        }
        .SBSE_container > div > a { display: inline-block; text-align: center; }
        .SBSE_container label { margin-right: 10px; }
        #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
    GM_addStyle(`
        .SBSE_container > div > button:before {
            content: '';
            position: absolute;
            margin-top: 5px;
            right: 10px;
            width: 20px;
            height: 20px;
            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); }
        }
    `);

    const $container = $(`
        <div class="SBSE_container">
            <textarea></textarea>
            <div>
                <button class="SBSE_BtnReveal">${text.buttonReveal}</button>
                <button class="SBSE_BtnRetrieve">${text.buttonRetrieve}</button>
                <button class="SBSE_BtnActivate">${text.buttonActivate}</button>
                <button class="SBSE_BtnCopy">${text.buttonCopy}</button>
                <button class="SBSE_BtnReset">${text.buttonReset}</button>
                <a class="SBSE_BtnExport">${text.buttonExport}</a>
                <label><input type="checkbox" class="SBSE_ChkTitle">${text.checkboxIncludeGameTitle}</label>
                <label><input type="checkbox" class="SBSE_ChkJoin">${text.checkboxJoinKeys}</label>
                <button id="SBSE_BtnSettings"> </button>
            </div>
        </div>
    `);

    // bind event
    $container.find('.SBSE_BtnCopy').click(bundleSitesBoxHandler.copy);
    $container.find('.SBSE_BtnReset').click(bundleSitesBoxHandler.reset);
    $container.find('.SBSE_BtnActivate').click(bundleSitesBoxHandler.activate);
    $container.find('#SBSE_BtnSettings').click(bundleSitesBoxHandler.settings);

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

    return $container;
};
const siteCache = {
    fanatical: {
        doms: ['.account-content']
    },
    bundlestars: {
        doms: [document]
    }
};
const siteHandlers = {
    indiegala() {
        const $box = bundleSitesBox();
        // insert textarea
        $('#library-contain').eq(0).before($box);

        // 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') {
                            $node.prepend($box);
                            observer.disconnect();
                        }
                    });
                });
            });

            observer.observe($node[0], { childList: true });
        }

        // inject css
        GM_addStyle(`
            .SBSE_container { margin-top: 10px; }
            .SBSE_container > textarea { border: 1px solid #CC001D; border-radius: 3px; }
            .SBSE_container > div > button, .SBSE_container > div > a { width: 100px; background-color: #CC001D; color: white; border-radius: 3px; }
            .SBSE_container > div > a:hover { color: white; }
        `);

        // dom source
        const source = location.pathname === '/profile' ? 'div[id*="_sale_"].collapse.in' : document;
        const extractKeys = () => {
            const keys = [];

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

                if (key) {
                    const $a = $ele.find('.title_game > a');
                    const title = $a.text().trim();

                    // append key details
                    keyDetails.set(key, {
                        url: $a.attr('href'),
                        title: $a.text()
                    });

                    keys.push({
                        key,
                        title
                    });
                }
            });

            return keys;
        };

        // button click
        $box.find('.SBSE_BtnReveal').click(() => {
            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];

                    $.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(text.failTitle, text.failDetailUnexpected, 'error');
                        }
                    });
                } else callback();
            };

            bundleSitesBoxHandler.reveal(handler, $(source).find('a[id^=fetchlink_]'));
        });
        $box.find('.SBSE_BtnRetrieve').click(() => {
            bundleSitesBoxHandler.retrieve(extractKeys());
        });
        $box.find('.SBSE_BtnExport').click(() => {
            const $bundleTitle = location.pathname === '/profile' ? $('[aria-expanded="true"] > div#bundle-title') : $('#bundle-title, #indie_gala_2 > div > span');
            const title = `IndieGala ${$bundleTitle.length > 0 ? $bundleTitle.text() : 'Keys'}`;
            bundleSitesBoxHandler.export(extractKeys(), title);
        });
    },
    fanatical() {
        /*
        const cache = siteCache.fanatical;
        const selectGames = (selector) => {
        let $results = $();
        let from = parseInt($('.SBSE_container .selectFrom').val(), 10);
        let to = parseInt($('.SBSE_container .selectTo').val(), 10);
        if ($.isNumeric(from) && $.isNumeric(to)) {
        if (from === 0 && to > 0) from = 1;
        if (from > 0 && to === 0) to = cache.doms.length - 1;
        for (let i = Math.min(from, to); i <= Math.max(from, to); i += 1) {
          $results = $results.add(
              $(cache.doms[i]).find(selector),
          );
        }
        }
        return $results;
        };*/
        const extractKeys = () => {
            const keys = [];

            $('.account-content dl input').each((index, input) => {
                const $input = $(input);

                keys.push({
                    key: $input.val(),
                    title: $input.closest('dd').prev().text().trim()
                });
            });

            return keys;
        };
        const insertBox = () => {
            const $anchor = $('h3:contains(Order Keys)');

            if ($('.SBSE_container').length === 0 && $anchor.length > 0) {
                // insert textarea
                $anchor.eq(0).before(bundleSitesBox());

                // inject css
                GM_addStyle(`
                    .SBSE_container { margin-top: 10px; }
                    .SBSE_container > textarea { background-color: #434343; color: #eee; }
                    .SBSE_container > div > button, .SBSE_container > div > a { width: 80px; }
                    .SBSE_container > div > button, .SBSE_container select, .SBSE_container > div > a { border: 1px solid transparent; background-color: #1c1c1c; color: #eee; }
                    .SBSE_container > div > button:hover, .SBSE_container select:hover, .SBSE_container > div > a:hover { color: #A8A8A8; }
                    .SBSE_container > div > a { text-decoration: none; }
                    .SBSE_container label { color: #DEDEDE; }
                    .SBSE_container select { max-width:120px; height: 30px; }
                    .SBSE_container select, .SBSE_container span { margin-right: 0; margin-left: 10px; float: right; }
                    .SBSE_container span { margin-top: 5px; }
                `);

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

                // dodge from master css selector
                $('.SBSE_container > div > a').attr('href', '');
                /*
                // insert bundlestars select
                $('.SBSE_container > div').append(`
                    <select class="selectTo"></select>
                    <span>${text.selectConnector}</span>
                    <select class="selectFrom"></select>
                `);
                */
                // button click
                $('.SBSE_BtnReveal').click(() => {
                    const handler = ($games, callback) => {
                        const game = $games.shift();

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

                    bundleSitesBoxHandler.reveal(handler, $('.account-content dl button'));
                });

                $('.SBSE_BtnRetrieve').click(() => {
                    bundleSitesBoxHandler.retrieve(extractKeys());
                });
                $('.SBSE_BtnExport').click(() => {
                    const $bundleTitle = $('h5');
                    const title = `Fanatical - ${$bundleTitle.length > 0 ? $bundleTitle.text() : 'Keys'}`;

                    bundleSitesBoxHandler.export(extractKeys(), title);
                });
                /*
                // setup select
                const $selects = $('.SBSE_container select');
                 $selects.empty();
                $selects.append(new Option('All', 0));
                 // individual games
                $anchor.parent().children('dl').each((index, dl) => {
                    $selects.append(
                        new Option($(dl).children().eq(1).text(), cache.doms.push(dl) - 1),
                    );
                });
                 // bundles
                $anchor.parent().find('h5').each((index, bundle) => {
                    const $bundle = $(bundle);
                    const $tiers = $bundle.parent().find('h6');
                    const bundleTitle = $bundle.text();
                     if ($tiers.length > 0) {
                        $tiers.each((i, tier) => {
                            const tierTitle = $(tier).text().trim();
                             $selects.append(
                                new Option(`${bundleTitle} ${tierTitle}`, cache.doms.push(tier.parentNode) - 1),
                            );
                        });
                    } else {
                        $selects.append(
                            new Option(bundleTitle, cache.doms.push(bundle.parentNode) - 1),
                        );
                    }
                });*/
            }
        };

        new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                Array.from(mutation.addedNodes).forEach(addedNode => {
                    if (addedNode.tagName === 'META' && addedNode.name === 'twitter:title') insertBox();
                });
            });
        }).observe($('head')[0], { childList: true });
    },
    bundlestars(firstCalled) {
        const cache = siteCache.bundlestars;
        const $anchor = $('h2:contains(Order Keys)');
        const BSselect = selector => {
            let $results = $();
            let from = parseInt($('.SBSE_container .selectFrom').val(), 10);
            let to = parseInt($('.SBSE_container .selectTo').val(), 10);

            if ($.isNumeric(from) && $.isNumeric(to)) {
                if (from === 0 && to > 0) from = 1;
                if (from > 0 && to === 0) to = cache.doms.length - 1;

                for (let i = Math.min(from, to); i <= Math.max(from, to); i += 1) {
                    $results = $results.add($(cache.doms[i]).find(selector));
                }
            }

            return $results;
        };
        const extractKeys = () => {
            const keys = [];

            BSselect('.key-container input').each((index, input) => {
                const $input = $(input);

                keys.push({
                    key: $input.val(),
                    title: $input.closest('.key-container').prev().text().trim()
                });
            });

            return keys;
        };

        if ($('.SBSE_container').length === 0 && $anchor.length > 0) {
            // insert textarea
            $anchor.eq(0).before(bundleSitesBox());

            // inject css
            GM_addStyle(`
                .SBSE_container { border: 1px solid #424242; color: #999999; }
                .SBSE_container > textarea { background-color: #303030; color: #DDD; }
                .SBSE_container > div > button, .SBSE_container > div > a { width: 80px; }
                .SBSE_container > div > button, .SBSE_container select, .SBSE_container > div > a { border: 1px solid transparent; background-color: #262626; color: #DEDEDE; }
                .SBSE_container > div > button:hover, .SBSE_container select:hover, .SBSE_container > div > a:hover { color: #A8A8A8; }
                .SBSE_container > div > a { text-decoration: none; }
                .SBSE_container label { color: #DEDEDE; }
                .SBSE_container select { max-width:120px; height: 30px; }
                .SBSE_container select, .SBSE_container span { margin-right: 0; margin-left: 10px; float: right; }
                .SBSE_container span { margin-top: 5px; }
            `);

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

            // insert bundlestars select
            $('.SBSE_container > div').append(`
                <select class="selectTo"></select>
                <span>${text.selectConnector}</span>
                <select class="selectFrom"></select>
            `);

            // button click
            $('.SBSE_BtnReveal').click(() => {
                const handler = ($games, callback) => {
                    const game = $games.shift();

                    if (game) {
                        if (!game.closest('.ng-hide')) {
                            game.click();
                            setTimeout(handler.bind(null, $games, callback), 300);
                        } else handler($games, callback);
                    } else setTimeout(callback, 500);
                };

                bundleSitesBoxHandler.reveal(handler, BSselect('.key-container a[ng-click^="redeemSerial"]'));
            });

            $('.SBSE_BtnRetrieve').click(() => {
                bundleSitesBoxHandler.retrieve(extractKeys());
            });
            $('.SBSE_BtnExport').click(() => {
                const $bundleTitle = $('h3.bundle');
                const title = `BundleStars - ${$bundleTitle.length > 0 ? $bundleTitle.text() : 'Keys'}`;

                bundleSitesBoxHandler.export(extractKeys(), title);
            });
        }

        // setup select
        const $selects = $('.SBSE_container select');

        $selects.empty();
        $selects.append(new Option('All', 0));
        cache.doms = [document];
        $('hr ~ div > div:not(.ng-hide)').each((index, block) => {
            const $block = $(block);
            const $bundle = $block.find('h3');
            const $tiers = $block.find('h4');

            if ($tiers.length > 1) {
                // bundles with multiple tiers
                $tiers.each((i, tier) => {
                    const $tier = $(tier);

                    $selects.append(new Option(`${$bundle.text()} ${$tier.text()}`, cache.doms.push($tier.parent()) - 1));
                });
            } else if ($bundle.length > 0) {
                // bundles with single tier
                $selects.append(new Option($bundle.text(), cache.doms.push($bundle.next()) - 1));
            } else {
                // individual games
                $selects.append(new Option($block.find('.title').text(), cache.doms.push($block) - 1));
            }
        });

        if (firstCalled) {
            new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    Array.from(mutation.removedNodes).forEach(removedNode => {
                        if (removedNode.id === 'loading-bar-spinner') siteHandlers.bundlestars();
                    });
                });
            }).observe(document.body, { childList: true });
        }
    },
    humblebundle() {
        let atDownload = true;
        const $box = bundleSitesBox();
        const fetchKey = ($node, machineName, callback) => {
            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: `gift=false&keytype=${machineName}&key=${unsafeWindow.gamekeys[0]}&keyindex=0`,
                credentials: 'same-origin'
            }).then(res => {
                if (res.ok) return res.json();
                throw new Error('Network response was not ok.');
            }).then(d => {
                if (d.success) {
                    $node.closest('.sr-unredeemed').replaceWith(`
                        <div class="sr-redeemed">
                            <div class="sr-redeemed">
                                <div class="sr-redeemed-bubble js-sr-redeemed-bubble ">
                                    <div class="keyfield-text">${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>
                            </div>
                        </div>
                    `);
                } else swal(text.failTitle, JSON.stringify(d), 'error');

                if (typeof callback === 'function') callback();
            });
        };
        const extractKeys = () => {
            const skipOwned = !!$('.SBSE_ChkSkipOwned:checked').length;
            const keys = [];
            const selectors = ['.sr-redeemed-bubble .keyfield-text', // redeem page selector
            'tr:has(.hb-steam) .redeemed > .keyfield-value'];

            if (skipOwned) selectors[0] = `div:not(.SBSE_owned) > .sr-key ${selectors[0]}`;

            $(selectors.join()).each((index, element) => {
                const $game = $(element);
                const $heading = atDownload ? $game.closest('[class^=sr-key]').prev().children().eq(0) : $game.closest('td').prev('.game-name').find('h4');

                keys.push({
                    key: $game.text().trim(),
                    title: $heading.text().trim()
                });
            });

            return keys;
        };
        const $keyManager = $('.js-key-manager-holder');

        // insert textarea
        // insert at /home/*
        if ($keyManager.length > 0) {
            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    Array.from(mutation.addedNodes).forEach(addedNode => {
                        if (addedNode.className === 'header') {
                            atDownload = false;

                            $(addedNode).after($box);
                            $box.find('.SBSE_ChkSkipOwned').parent().remove();

                            observer.disconnect();
                        }
                    });
                });
            });

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

                        if ($node.hasClass('sr-widget') && $node.closest('.keytab').length > 0) {
                            $node.closest('.whitebox-redux').before($box);

                            // fetch key data
                            const $script = $('.steam-keyredeemer-container').next();

                            if ($script.length > 0) {
                                let data = $script.text().split('var data = ').pop().split(';').shift();

                                try {
                                    data = JSON.parse(data);

                                    data.keys.forEach(key => {
                                        keyDetails.set(key.redeemedKeyVal, {
                                            app: key.steamAppId,
                                            title: key.humanName
                                        });

                                        const $heading = $node.find(`.sr-key-heading:has(span[data-machine-name=${key.machineName}])`);

                                        // apply owned effect on game title
                                        const appID = parseInt(key.steamAppId, 10);

                                        if (appID && steam.owned.app.includes(appID)) $heading.parent().parent().addClass('SBSE_owned');

                                        // activation restrictions
                                        let html = '';
                                        const disallowed = key.disallowedCountries.map(c => ISO2.get(c));
                                        const exclusive = key.exclusiveCountries.map(c => ISO2.get(c));
                                        const humanName = $heading.text().trim();
                                        const separator = config.get('language').includes('chinese') ? '、' : ', ';

                                        if (disallowed.length > 0) html += `<p>${text.HBDisallowedCountries}<br>${disallowed.join(separator)}</p>`;
                                        if (exclusive.length > 0) html += `<p>${text.HBExclusiveCountries}<br>${exclusive.join(separator)}</p>`;
                                        if (disallowed.length > 0 || exclusive.length > 0) {
                                            $(`<span class="SBSE_activationRestrictions">${text.HBActivationRestrictions}</span>`).click(() => {
                                                swal({
                                                    title: `${humanName}<br>${text.HBActivationRestrictions}`,
                                                    html,
                                                    type: 'info'
                                                });
                                            }).insertBefore($heading.parent().parent());
                                        }
                                    });
                                } catch (e) {
                                    // no key details
                                }
                            }

                            observer.disconnect();
                        }
                    });
                });
            });

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

        // inject css
        GM_addStyle(`
            .SBSE_container > div { position: relative; }
            .SBSE_container > textarea {
                border: 1px solid #CFCFCF;
                color: #4a4c45;
                text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
                border-radius: 5px;
            }
            .SBSE_container > div > button, .SBSE_container > div > a {
                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, .SBSE_container > div > a: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 .sr-unredeemed-steam-button * { opacity: 0.37; }
            .SBSE_owned .sr-key-heading > span:last-child::after {
                content: '\\f085';
                font-family: hb-icons;
                color: #17A1E5;
            }
            .SBSE_activationRestrictions {
                float: right;
                cursor: pointer;
            }
        `);

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

        // append checkbox for owned game
        $box.find('#SBSE_BtnSettings').before($(`<label><input type="checkbox" class="SBSE_ChkSkipOwned" checked>${text.checkboxSkipOwned}</label>`));

        // button click
        $box.find('.SBSE_BtnReveal').click(() => {
            const skipOwned = !!$('.SBSE_ChkSkipOwned:checked').length;
            const handler = ($games, callback) => {
                const game = $games.shift();

                if (game) {
                    const $game = $(game);
                    const isOwned = !!$game.closest('.SBSE_owned').length;
                    const machineName = $game.closest('.sr-key').find('.js-admin-edit').data('machine-name');

                    if (skipOwned && isOwned) handler($games, callback);else if (atDownload && machineName) {
                        fetchKey($game, machineName, () => {
                            handler($games, callback);
                        });
                    } else {
                        game.click();
                        $('.sr-warning-modal-confirm-button').click();

                        setTimeout(handler.bind(null, $games, callback), 200);
                    }
                } else callback();
            };
            const selectors = ['.sr-unredeemed-steam-button', // redeem page selector
            'div.keyfield[title="Reveal your Steam key"]'];

            if (skipOwned) selectors[0] = `div:not(.SBSE_owned) > .sr-key ${selectors[0]}`;

            bundleSitesBoxHandler.reveal(handler, $(selectors.join()));
        });
        $box.find('.SBSE_BtnRetrieve').click(() => {
            bundleSitesBoxHandler.retrieve(extractKeys());
        });
        $box.find('.SBSE_BtnExport').click(() => {
            const $bundleTitle = $('meta[name=title]');
            const title = `Humble Bundle - ${$bundleTitle.length > 0 ? $bundleTitle.attr('content') : 'Keys'}`;

            bundleSitesBoxHandler.export(extractKeys(), title);
        });

        // override default popups
        document.addEventListener('click', e => {
            const $target = $(e.target).closest('.sr-unredeemed-steam-button');

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

                const $heading = $target.closest('.sr-key').find('.sr-key-heading');
                const machineName = $heading.find('.js-admin-edit').data('machine-name');
                const humanName = $heading.text().trim();
                const isOwned = !!$target.closest('.SBSE_owned').length;

                if (machineName) {
                    if (isOwned) {
                        swal({
                            title: text.HBAlreadyOwned,
                            text: text.HBRedeemAlreadyOwned.replace('%title%', humanName),
                            type: 'question',
                            showCancelButton: true
                        }).then(result => {
                            if (result.value) fetchKey($target, machineName);
                        });
                    } else fetchKey($target, machineName);
                }
            }
        }, true);
    },
    dailyindiegame() {
        const MPHideList = JSON.parse(GM_getValue('SBSE_DIGMPHideList') || '[]');
        const pathname = location.pathname;

        if (pathname.includes('/account_page')) {
            // force sync library
            syncLibrary(false);

            // 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 extractKeys = () => {
                const keys = [];
                const marketListings = !!$('.SBSE_ChkMarketListings:checked').length;

                $('#TableKeys tr').each((index, tr) => {
                    const $tds = $(tr).children();

                    if ($tds.eq(4).text().includes('-') && (!$tds.eq(6).text().includes('Cancel trade') || marketListings)) {
                        keys.push({
                            key: $tds.eq(4).text().trim(),
                            title: $tds.eq(2).text().trim()
                        });
                    }
                });

                return keys;
            };

            // insert textarea
            const $box = bundleSitesBox();
            $('#TableKeys').eq(0).before($box);

            // append checkbox for market keys
            $box.find('#SBSE_BtnSettings').before($(`<label><input type="checkbox" class="SBSE_ChkMarketListings">${text.checkboxMarketListings}</label>`));

            // button click
            $('.SBSE_BtnReveal').click(() => {
                const handler = () => {
                    const $form = $('#form3');

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

                bundleSitesBoxHandler.reveal(handler);
            });
            $('.SBSE_BtnRetrieve').click(() => {
                bundleSitesBoxHandler.retrieve(extractKeys());
            });
            $('.SBSE_BtnExport').remove();

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

            if ($awaitRatings.length > 0) {
                $('#TableKeys td:contains(Rate TRADE)').text(text.DIGRateAllPositive).css('cursor', 'pointer').click(() => {
                    $awaitRatings.each((index, a) => {
                        fetch(a.href, {
                            method: 'GET',
                            credentials: 'same-origin'
                        }).then(res => {
                            if (res.ok) {
                                $(a).parent('td').html('<span class="DIG3_14_Orange">Positive</span>');
                            }
                        });
                    });
                });
            }
        } else if (pathname === '/account_digstore.html' || pathname === '/account_trades.html' || pathname === '/account_tradesXT.html') {
            // DIG EasyBuy
            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;}
            `);

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

                $row.attr('data-id', $game.attr('href').replace(/\D/g, ''));
                $row.attr('data-price', parseInt($row.find('td:contains(DIG Points)').text(), 10) || 0);
                $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);

                if (steam.owned[data[0]].includes(steamID)) $tr.addClass('SBSE_owned SBSE_hide');
                if (steam.wished[data[0]].includes(steamID)) $tr.addClass('SBSE_wished');
                if (steam.ignored[data[0]].includes(steamID)) $tr.addClass('SBSE_ignored');
                if (MPHideList.includes(id)) $tr.addClass('SBSE_hide');

                // append manual hide feature
                $tr.children().eq(0).attr('title', text.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">${text.DIGEasyBuyPurchase}</button>
                    <button class="DIGButtonSelectAll DIG3_Orange_15_Form">${text.DIGEasyBuySelectAll}</button>
                    <button class="DIGButtonHideOwned DIG3_Orange_15_Form">${text.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">${text.DIGEasyBuyLoadAllPages}</button>
                `);
            }

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

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

            // bind button event
            $('.DIGButtonPurchase').click(e => {
                let bought = 0;
                let balance = parseFloat($('a[href^="account_transac"]').parent('div').text().slice(19)) || 0;
                const $self = $(e.delegateTarget);
                const $checked = $('.DIGEasyBuy_checked');
                const handler = callback => {
                    const item = $checked.shift();

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

                        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;
                                }

                                fetch(url, requestInit).then(res => {
                                    if (res.ok) {
                                        $item.click();
                                        bought += 1;
                                        balance -= price;

                                        setTimeout(handler.bind(null, callback), 300);
                                    } else handler(callback);
                                });
                            } else {
                                swal({
                                    title: text.failTitle,
                                    text: text.DIGInsufficientFund,
                                    type: 'error'
                                }).then(() => {
                                    window.location = `${location.origin}/account_page.html`;
                                });
                            }
                        } else handler(callback);
                    } else callback();
                };

                $self.prop('disabled', true).text(text.DIGButtonPurchasing);
                handler(() => {
                    if (bought) window.location = `${location.origin}/account_page.html`;else $self.prop('disabled', false).text(text.DIGButtonPurchase);
                });
            });
            $('.DIGButtonSelectAll').click(e => {
                const $self = $(e.delegateTarget);
                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 ? text.DIGEasyBuySelectCancel : text.DIGEasyBuySelectAll);
            });
            $('.DIGButtonHideOwned').click(e => {
                const $self = $(e.delegateTarget);
                const state = !$self.data('state');

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

                    fetch(`${location.origin}/account_tradesXT_${page}.html`, {
                        method: 'GET',
                        credentials: 'same-origin'
                    }).then(res => {
                        if (res.ok) return res.text();
                        throw new Error('Network response was not ok.');
                    }).then(html => {
                        const $result = $(html).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(text.DIGEasyBuyLoadingComplete);
                    }).catch(() => {
                        if (retry < 3) load(page, retry + 1);else load(page + 1);
                    });
                };

                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: 50
                });

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

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

                $('.tt-dropdown-menu').click(() => {
                    $searchResult.empty();
                    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'
                    }).then(res => {
                        if (res.ok) return res.text();
                        throw new Error('Network response was not ok.');
                    }).then(html => {
                        $searchResult.append($(html).find('#TableKeys'));
                    });
                });
                // result page
            } else {
                GM_addStyle(`
                    .check.icon {
                        color: #5cb85c;
                        margin: 12px 0 5px 9px;
                        width: 42px;
                        height: 24px;
                        border-bottom: solid 3px currentColor;
                        border-left: solid 3px currentColor;
                        transform: rotate(-45deg);
                    }
                    .remove.icon { color: #d9534f; margin-left: 9px; margin-top: 30px; }
                    .remove.icon:before, .remove.icon:after {
                        content: '';
                        position: absolute;
                        width: 45px;
                        height: 3px;
                        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() {
        // insert textarea
        $('.featurette-divider').eq(0).after(bundleSitesBox());

        // inject css
        GM_addStyle(`
            .SBSE_container {
                width: 80%;
                margin: 0 auto;
                color: #000;
                font-size: 16px;
            }
            .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, .SBSE_container > div > a {
                width: 80px;
                border: 1px solid #2e6da4;
                border-radius: 5px;
                background-color: #337ab7;
                color: #FFF;
            }
            .SBSE_container > div > a:hover { text-decoration: none; opacity: 0.9; }
            .SBSE_container label { color: #EEE; }
            .expanded .showOrderMeta {
                display: block !important;
                position: absolute;
                margin-top: -8px;
                right: 265px;
                z-index: 1;
            }
        `);

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

        const extractKeys = () => {
            const keys = [];

            $('.deliver-gkey:contains(-)').each((index, element) => {
                const $game = $(element);

                keys.push({
                    key: $game.text().trim(),
                    title: $game.parent().prev().text().trim()
                });
            });

            return keys;
        };

        // button click
        $('.SBSE_BtnReveal').click(() => {
            const handler = ($games, callback) => {
                const game = $games.shift();

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

            bundleSitesBoxHandler.reveal(handler, $('.deliver-btn'));
        });
        $('.SBSE_BtnRetrieve').click(() => {
            bundleSitesBoxHandler.retrieve(extractKeys());
        });
        $('.SBSE_BtnExport').click(() => {
            const bundleTitle = 'CCYYCN Bundle'; // can't find bundle title in html

            bundleSitesBoxHandler.export(extractKeys(), bundleTitle);
        });
    },
    groupees() {
        if (location.pathname.startsWith('/profile/')) {
            // insert textarea
            $('.table-products').before(bundleSitesBox());

            // inject css
            GM_addStyle(`
                .SBSE_container > textarea, .SBSE_container > div > button, .SBSE_container > div > a {
                    background: transparent;
                    border: 1px solid #8cc53f;
                    border-radius: 3px;
                    color: #8cc53f;
                    transition: all 0.8s ease;
                }
                .SBSE_container > div > button:hover, .SBSE_container > div > a:hover {
                    background-color: #8cc53f;
                    color: white;
                    text-decoration: none;
                }
                img.product-cover { display: none; }
            `);

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

            const extractKeys = () => {
                const skipUsed = !!$('.SBSE_ChkSkipUsed:checked').length;
                const keys = [];

                $('.key-block input.code').each((index, element) => {
                    const $game = $(element);
                    const used = !!$game.closest('.key-block').find('.key-status:contains(used)').length;

                    if ($game.val().includes('-') && (!used || used && !skipUsed)) {
                        keys.push({
                            key: $game.val(),
                            title: $game.closest('tr').prev().children('td').eq(2).text().trim()
                        });
                    }
                });

                return keys;
            };

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

            // button click
            $('.SBSE_BtnReveal').click(() => {
                const handler = ($games, callback) => {
                    const game = $games.shift();

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

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

                $reveals.click();
                setTimeout(() => {
                    bundleSitesBoxHandler.reveal(handler, $('.btn-reveal-key'));
                }, timer);
            });
            $('.SBSE_BtnRetrieve').click(() => {
                bundleSitesBoxHandler.retrieve(extractKeys());
            });
            $('.SBSE_BtnExport').click(() => {
                const bundleTitle = `Groupees - ${$('h2').text()}`;

                bundleSitesBoxHandler.export(extractKeys(), bundleTitle);
            });

            // bind custom event
            $(document).on('activated', (e, key, result) => {
                if (result.success === 1) $(`.btn-steam-redeem[href*=${key}]`).next('.key-usage-toggler').click();
            });
        } else {
            // insert textarea
            $('.container > div').eq(1).before(bundleSitesBox());

            // inject css
            GM_addStyle(`
                .SBSE_container { margin-bottom: 20px; }
                .SBSE_container > textarea { background-color: #EEE; border-radius: 3px; }
                .SBSE_container > div > button, .SBSE_container > div > a { outline: none !important; }
                #SBSE_BtnSettings { margin-top: 8px; }
            `);

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

            // add buttons style via groupees's class
            $('.SBSE_container > div > button, .SBSE_container > div > a').addClass('btn btn-default');

            // 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>${text.markAllAsUsed}</b></button>`).click(() => {
                                $('.expanded .usage').each((i, checkbox) => {
                                    if (!checkbox.checked) checkbox.click();
                                });
                            }));
                            $orderMeta.parent().addClass('showOrderMeta');
                        }
                    });
                });
            }).observe($('#profile_content')[0], { childList: true });

            const extractKeys = () => {
                const skipUsed = !!$('.SBSE_ChkSkipUsed:checked').length;
                const keys = [];

                $('.expanded .code').each((index, element) => {
                    const $game = $(element);
                    const used = $game.closest('li').find('.usage').prop('checked');

                    if (!used || used && !skipUsed) {
                        keys.push({
                            key: $game.val(),
                            title: $game.closest('.details').find('h3').text().trim()
                        });
                    }
                });

                return keys;
            };

            // button click
            $('.SBSE_BtnReveal').click(() => {
                const handler = ($games, callback) => {
                    const game = $games.shift();

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

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

                $reveals.click();
                setTimeout(() => {
                    bundleSitesBoxHandler.reveal(handler, $('.expanded .reveal'));
                }, timer);
            });
            $('.SBSE_BtnRetrieve').click(() => {
                bundleSitesBoxHandler.retrieve(extractKeys());
            });
            $('.SBSE_BtnExport').click(() => {
                const bundleTitle = `Groupees - ${$('.expanded .caption').text()}`;

                bundleSitesBoxHandler.export(extractKeys(), bundleTitle);
            });

            // 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) {
            // insert textarea
            $('#tabs').eq(0).prepend(bundleSitesBox());

            // inject css
            GM_addStyle(`
                .SBSE_container > textarea { border: 1px solid #AAAAAA; }
                .SBSE_container > div > button, .SBSE_container > div > a {
                    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, .SBSE_container > div > a:hover {
                    border-color: #999999;
                    background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;
                    color: #212121;
                }
                #SBSE_BtnSettings { width: 32px !important; height: 32px !important; }
            `);

            // remove event from agiso
            $('.SBSE_container > div > button').click(e => {
                e.preventDefault();
            });

            // hide reveal
            $('.SBSE_BtnReveal').hide();

            // button click
            $('.SBSE_BtnRetrieve').click(() => {
                bundleSitesBoxHandler.retrieve(keys);
            });
            $('.SBSE_BtnExport').click(() => {
                const bundleTitle = 'agiso Bundle';

                bundleSitesBoxHandler.export(keys, bundleTitle);
            });
        }
    }
};
const 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: text.updateSuccessTitle,
                            text: text.updateSuccess,
                            type: 'success',
                            timer: 3000
                        });
                    });
                }
            }
        }
        /* else {
            swal(text.notLoggedInTitle, text.notLoggedInMsg, 'error');
        } */
    } else {
        const site = location.hostname.replace(/(www|alds|bundle)\./, '').split('.').shift();

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

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

            // update steam library every 10 min
            const updateTimer = 10 * 60 * 1000;
            if (!steam.lastSync || steam.lastSync < Date.now() - updateTimer) syncLibrary(false);
        }
    }
};

$(init);