// ==UserScript==
// @name Steam Bundle Sites Extension
// @homepage https://github.com/clancy-chao/Steam-Bundle-Sites-Extension
// @namespace http://tampermonkey.net/
// @version 2.16.6
// @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/library*
// @include https://www.indiegala.com/game*
// @include https://www.fanatical.com/*
// @include https://www.humblebundle.com/*
// @include http*://*dailyindiegame.com/*
// @include http*://www.ccyyshop.com/order/*
// @include https://groupees.com/purchases
// @include https://groupees.com/profile/purchases/*
// @include http*://*agiso.com/*
// @include https://steamdb.keylol.com/tooltip*
// @include https://yuplay.ru/orders/*/
// @include https://yuplay.ru/product/*/
// @include http*://gama-gama.ru/personal/settings/*
// @include http*://*plati.ru/seller/*
// @include http*://*plati.market/seller/*
// @include http*://*plati.ru/cat/*
// @include http*://*plati.market/cat/*
// @exclude http*://store.steampowered.com/widget/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.18.0/sweetalert2.min.js
// @resource sweetalert2CSS https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.18.0/sweetalert2.min.css
// @resource currencyFlags https://cdnjs.cloudflare.com/ajax/libs/currency-flags/1.5.0/currency-flags.min.css
// @resource flagIcon https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/css/flag-icon.min.css
// @connect store.steampowered.com
// @connect www.google.com
// @connect www.google.com.tw
// @connect www.google.com.au
// @connect www.google.co.jp
// @connect www.google.co.nz
// @connect www.google.co.uk
// @connect www.google.ca
// @connect www.google.de
// @connect www.google.it
// @connect www.google.fr
// @connect www.ecb.europa.eu
// @connect steamdb.keylol.com
// @connect steamdb.info
// @connect steamspy.com
// @connect github.com
// @connect localhost
// @connect 127.0.0.1
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
/* global swal */
// inject external css styles
GM_addStyle(GM_getResourceText('sweetalert2CSS'));
GM_addStyle(GM_getResourceText('currencyFlags'));
GM_addStyle(GM_getResourceText('flagIcon').replace(/\.\.\//g, 'https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/3.1.0/'));
// inject script css styles
GM_addStyle(`
pre.SBSE-errorMsg { height: 200px; text-align: left; white-space: pre-wrap; }
a.SBSE-link-steam_store, a.SBSE-link-steam_db { text-decoration: none; font-size: smaller; }
a.SBSE-link-steam_store:hover, a.SBSE-link-steam_db:hover { text-decoration: none; }
/* switch */
.SBSE-switch { position: relative; display: inline-block; width: 60px; height: 30px; }
.SBSE-switch input { display: none; }
.SBSE-switch__slider {
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
background-color: #CCC;
transition: 0.4s;
cursor: pointer;
}
.SBSE-switch__slider:before {
width: 26px; height: 26px;
position: absolute;
bottom: 2px; left: 2px;
background-color: white;
transition: 0.4s;
content: "";
}
.SBSE-switch input:checked + .SBSE-switch__slider { background-color: #2196F3; }
.SBSE-switch input:focus + .SBSE-switch__slider { box-shadow: 0 0 1px #2196F3; }
.SBSE-switch input:checked + .SBSE-switch__slider:before { transform: translateX(30px); }
.SBSE-switch--small { width: 40px; height: 20px; }
.SBSE-switch--small .SBSE-switch__slider:before { width: 16px; height: 16px; }
.SBSE-switch--small input:checked + .SBSE-switch__slider:before { transform: translateX(20px); }
/* dropdown */
.SBSE-dropdown { display: inline-block; position: relative; }
.SBSE-dropdown__list {
width: calc(100% - 10px);
max-height: 0;
display: inline-block;
position: absolute;
top: 35px; left: 0;
padding: 0;
transition: all 0.15s;
overflow: hidden;
list-style-type: none;
background-color: #EEE;
box-shadow: 1px 2px 3px rgba(0,0,0,0.45);
z-index: 999;
}
.SBSE-dropdown__list > li { width: 100%; display: block; padding: 3px 0; text-align: center; }
.SBSE-dropdown:hover > .SBSE-dropdown__list { max-height: 500px; }
/* grid */
.SBSE-grid { display: flex; flex-wrap: wrap; }
.SBSE-grid > span {
display: inline-block;
margin: 2px 10px;
padding: 0 5px;
border-radius: 5px;
cursor: pointer;
}
.SBSE-grid > .separator {
display: block;
width: 100%;
margin-top: 12px;
text-align: left;
font-weight: bold;
cursor: default;
}
.SBSE-grid > span.selected { background-color: antiquewhite; }
/* settings */
.SBSE-container__content__model[data-feature="setting"] .name { text-align: right; vertical-align: top; }
.SBSE-container__content__model[data-feature="setting"] .value { text-align: left; }
.SBSE-container__content__model[data-feature="setting"] .value > * { height: 30px; margin: 0 20px 10px; }
.SBSE-container__content__model[data-feature="setting"] > span { display: inline-block; color: white; cursor: pointer; }
/* container */
.SBSE-container { width: 100%; }
.SBSE-container__nav > ul { display: flex; margin: 0; padding: 0; list-style: none; }
.SBSE-container__nav__item { flex: 1 1 auto; text-align: center; cursor: pointer; }
.SBSE-container__nav__item--show { border-bottom: 1px solid #29B6F6; color: #29B6F6; }
.SBSE-container__nav__item > span { display: block; padding: 10px; }
.SBSE-container__content__model {
width: 100%; height: 200px;
display: flex;
margin-top: 10px;
flex-direction: column;
box-sizing: border-box;
}
.SBSE-container__content__model { display: none; }
.SBSE-container__content__model[data-feature="setting"] { height: 100%; display: block; }
.SBSE-container__content__model--show { display: block; }
.SBSE-container__content__model > textarea {
width: 100%; height: 150px;
padding: 5px;
border: none;
box-sizing: border-box;
resize: none;
outline: none;
}
.SBSE-container__content__model > div { width: 100%; padding-top: 5px; box-sizing: border-box; }
.SBSE-button {
width: 120px;
position: relative;
margin-right: 10px;
line-height: 28px;
transition: all 0.5s;
box-sizing: border-box;
outline: none;
cursor: pointer;
}
.SBSE-select { max-width:120px; height: 30px; }
.SBSE-container label { margin-right: 10px; }
.SBSE-dropdown__list-export a { text-decoration: none; color: #333; transition: color 0.3s ease; }
.SBSE-dropdown__list-export a:hover { text-decoration: none; color: #787878; }
.SBSE-button-setting {
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;
cursor: pointer;
}
/* terminal */
.SBSE-terminal {
height: 150px;
display: none;
margin: 0;
padding: 0;
background-color: #000;
}
.SBSE-terminal--show { display: block; }
.SBSE-terminal > div {
max-height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
background-color: transparent;
}
.SBSE-terminal > div > span {
display: inline-block;
padding-left: 20px;
color: #FFF;
text-indent: -20px;
}
.SBSE-terminal > div > span:before {
content: '>';
width: 20px;
display: inline-block;
text-align: center;
text-indent: 0;
}
.SBSE-terminal__message {}
.SBSE-terminal__input {
width: 100%;
position: relative;
order: 9999;
box-sizing: border-box;
}
.SBSE-terminal__input > input {
width: inherit;
max-width: calc(100% - 30px);
position: absolute;
top: 0; right: 0; bottom: 0; left: 20px;
padding: 0;
border: none;
outline: none;
background-color: transparent;
color: #FFF;
}
.SBSE-terminal__input > input:first-child { z-index: 9; }
.SBSE-terminal__input > input:last-child { z-index: 3; color: gray; }
/* spinner button affect */
.SBSE-button:before {
width: 20px; height: 20px;
content: '';
position: absolute;
margin-top: 5px;
right: 10px;
border: 3px solid;
border-left-color: transparent;
border-radius: 50%;
box-sizing: border-box;
opacity: 0;
transition: opacity 0.5s;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-name: rotate;
animation-timing-function: linear;
}
.SBSE-button.SBSE-button--narrow.SBSE-button--working {
width: 100px;
padding-right: 40px;
transition: all 0.5s;
}
.SBSE-button.SBSE-button--working:before {
transition-delay: 0.5s;
transition-duration: 1s;
opacity: 1;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* types */
.SBSE-type {
height: 20px;
display: none;
margin-right: 5px;
justify-content: center;
}
.SBSE-type:before, .SBSE-type:after {
content: '';
box-sizing: border-box;
pointer-events: none;
}
.SBSE-type:after { padding: 0 2px; }
.SBSE-item--game .SBSE-type { background-color: rgba(97,100,101,0.3); }
.SBSE-item--game .SBSE-type:after { content: 'Game'; }
.SBSE-item--DLC .SBSE-type { background-color: rgba(165,84,177,0.8); }
.SBSE-item--DLC .SBSE-type:before {
content: 'ꜜ';
width: 14px; height: 14px;
margin: 3px 0 0 2px;
border-radius: 50%;
background-color: #000;
color: rgba(165,84,177,0.8);
text-align: center;
font-size: 28px;
line-height: 28px;
}
.SBSE-item--DLC .SBSE-type:after { content: 'DLC'; }
.SBSE-item--package .SBSE-type { background-color: rgba(47,137,188,0.8); }
.SBSE-item--package .SBSE-type:after { content: 'Package'; }
.SBSE-item--steam .SBSE-type { display: flex; }
/* icons */
.SBSE-icon {
width: 20px; height: 20px;
display: none;
margin-left: 5px;
border-radius: 50%;
background-color: #E87A90;
transform: rotate(45deg);
}
.SBSE-icon:before, .SBSE-icon:after {
content: '';
width: 3px; height: 14px;
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 5px;
pointer-events: none;
}
.SBSE-icon:after { transform: translate(-50%, -50%) rotate(-90deg); }
.SBSE-item--owned .SBSE-icon { background-color: #9CCC65; }
.SBSE-item--owned .SBSE-icon:before, .SBSE-item--owned .SBSE-icon:after { transform: none; }
.SBSE-item--owned .SBSE-icon:before {
width: 3px; height: 11px;
top: 4px; left: 10px;
border-radius: 5px 5px 5px 0;
}
.SBSE-item--owned .SBSE-icon:after {
width: 5px; height: 3px;
top: 12px; left: 6px;
border-radius: 5px 0 0 5px;
}
.SBSE-item--wished .SBSE-icon { transform: rotate(0); background-color: #29B6F6; }
.SBSE-item--wished .SBSE-icon:before, .SBSE-item--wished .SBSE-icon:after {
width: 6px; height: 10px;
top: 5px; left: 10px;
border-radius: 6px 6px 0 0;
transform: rotate(-45deg);
transform-origin: 0 100%;
}
.SBSE-item--wished .SBSE-icon:after {
left: 4px;
transform: rotate(45deg);
transform-origin :100% 100%;
}
.SBSE-item--ignored .SBSE-icon { background-color: rgb(135, 173, 189); }
.SBSE-item--notApplicable .SBSE-icon { transform: rotate(0); background-color: rgb(248, 187, 134); }
.SBSE-item--notApplicable .SBSE-icon:before {
content: '?';
width: 0; height: 10px;
top: 5px; left: 7px;
color: white;
font-size: 16px; font-weight: 900;
}
.SBSE-item--notApplicable .SBSE-icon:after { display: none; }
.SBSE-item--fetching .SBSE-icon { transform: rotate(0); background-color: transparent; }
.SBSE-item--fetching .SBSE-icon:before {
width: 20px; height: 20px;
top: 0; left: 0;
border: 3px solid grey;
border-left-color: transparent;
border-radius: 50%;
box-sizing: border-box;
transition: opacity 0.5s;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-name: rotate;
animation-timing-function: linear;
}
.SBSE-item--fetching .SBSE-icon:after { display: none; }
.SBSE-item--notFetched .SBSE-icon { background-color: transparent; }
.SBSE-item--notFetched .SBSE-icon:before, .SBSE-item--notFetched .SBSE-icon:after { display: none; }
.SBSE-item--failed .SBSE-icon { transform: rotate(0); }
.SBSE-item--failed .SBSE-icon:before {
content: '!';
width: 0; height: 10px;
top: 5px; left: 8.5px;
color: white;
font-size: 16px; font-weight: 900;
}
.SBSE-item--failed .SBSE-icon:after { display: none; }
.SBSE-item--steam .SBSE-icon { display: inline-block; }
/* Steam Tooltip */
.SBSE-tooltip {
width: 308px;
display: none;
position: fixed;
overflow: hidden;
background: url(https://steamstore-a.akamaihd.net/public/images/v6/blue_body_darker_repeat.jpg) -700px center repeat-y scroll rgb(0, 0, 0);
border: 0;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
transition: all 0.5s;
z-index: 999;
}
.SBSE-tooltip--show{ display: block; }
/* Tooltip */
[tooltip]::before, [tooltip]::after {
position: absolute;
opacity: 0;
transition: all 0.15s ease;
}
[tooltip]::before {
width: max-content;
content: attr(tooltip);
top: calc(100% + 10px); left: 0;
padding: 10px;
color: #4a4c45;
background-color: white;
border-radius: 3px;
box-shadow: 1px 2px 3px rgba(0,0,0,0.45);
}
[tooltip]::after {
content: "";
top: calc(100% + 5px); left: 10px;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid white;
}
[tooltip]:hover::before, [tooltip]:hover::after { opacity: 1; }
[tooltip]:not([tooltip-persistent])::before, [tooltip]:not([tooltip-persistent])::after { pointer-events: none; }
`);
// load up
const regURL = /(https?:\/\/)?([.\w]*steam[-.\w]*){1}\/.*?(apps?|subs?){1}\/(\d+){1}(\/.*\/?)?/m;
const regKey = /(?:(?:([A-Z0-9])(?!\1{4})){5}-){2,5}[A-Z0-9]{5}/g;
const eol = "\n";
const tab = "\t";
const has = Object.prototype.hasOwnProperty;
const forEachAsync = (array, callback, lastIterationCallback) => {
if (!Array.isArray(array)) throw Error('Not an array');
if (typeof callback !== 'function') throw Error('Not an function');
const iterators = [...array.keys()];
const processor = (taskStartTime) => {
let taskFinishTime;
do {
const iterator = iterators.shift();
if (iterator in array) callback(array[iterator], iterator, array);
taskFinishTime = window.performance.now();
} while (taskFinishTime - taskStartTime < 1000 / 60);
if (iterators.length > 0) requestAnimationFrame(processor);
// finished iterating array
else if (typeof lastIterationCallback === 'function') lastIterationCallback();
};
requestAnimationFrame(processor);
};
const unique = a => [...new Set(a)];
const isArray = value => Array.isArray(value);
const isObject = value => Object(value) === value;
const request = options => new Promise((resolve, reject) => {
options.onerror = reject;
options.ontimeout = reject;
options.onload = resolve;
GM_xmlhttpRequest(options);
});
// setup jQuery
const $ = jQuery.noConflict(true);
$.fn.pop = [].pop;
$.fn.shift = [].shift;
$.fn.eachAsync = function eachAsync(callback, lastIterationCallback) {
forEachAsync(this.get(), callback, lastIterationCallback);
};
const config = {
data: JSON.parse(GM_getValue('SBSE_config', '{}')),
set(key, value, callback) {
this.data[key] = value;
GM_setValue('SBSE_config', JSON.stringify(this.data));
if (typeof callback === 'function') callback();
},
get(key) {
return has.call(this.data, key) ? this.data[key] : null;
},
init() {
if (!has.call(this.data, 'autoUpdateSessionID')) this.data.autoUpdateSessionID = true;
if (!has.call(this.data, 'autoSyncLibrary')) this.data.autoSyncLibrary = true;
if (!has.call(this.data, 'ASFFormat')) this.data.ASFFormat = false;
if (!has.call(this.data, 'titleComesLast')) this.data.titleComesLast = false;
if (!has.call(this.data, 'activateAllKeys')) this.data.activateAllKeys = false;
if (!has.call(this.data, 'enableTooltips')) this.data.enableTooltips = this.get('language') !== 'english';
if (!has.call(this.data, 'highlightedRegions')) this.data.highlightedRegions = ['CN', 'HK', 'TW'];
if (!has.call(this.data, 'enableASFIPC')) this.data.enableASFIPC = false;
if (!has.call(this.data, 'ASFWSProtocol')) this.data.ASFWSProtocol = 'ws';
if (!has.call(this.data, 'ASFIPCProtocol')) this.data.ASFIPCProtocol = 'http';
if (!has.call(this.data, 'ASFIPCServer')) this.data.ASFIPCServer = '127.0.0.1';
if (!has.call(this.data, 'ASFIPCPort')) this.data.ASFIPCPort = 1242;
if (!has.call(this.data, 'ASFIPCPassword')) this.data.ASFIPCPassword = '';
},
};
const i18n = {
data: {
tchinese: {
name: '正體中文',
updateSuccessTitle: '更新成功!',
updateSuccess: '成功更新Steam sessionID',
successStatus: '成功',
successTitle: '好極了!',
successDetail: '無資料',
skippedStatus: '跳過',
activatedDetail: '已啟動',
loadingSuccess: '加載完成!',
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',
settingsAutoSyncLibrary: '自動同步Steam 遊戲庫',
settingsSyncLibrary: '同步遊戲庫',
settingsSyncLibraryButton: '同步',
settingsLanguage: '語言',
settingsASFFormat: '啟用ASF 格式',
settingsTitleComesLast: '遊戲名置後',
settingsActivateAllKeys: '不跳過、啟動所有序號',
settingsEnableTooltips: 'Keylol 論壇提示框',
settingshighlightedRegions: '標示出地區',
settingshighlightedRegionsButton: '選擇地區',
settingsEnableASFIPC: '啟用ASF IPC',
settingsASFWSProtocol: 'ASF WS 傳輸協定',
settingsASFIPCProtocol: 'ASF IPC 傳輸協定',
settingsASFIPCServer: 'ASF IPC IP位址',
settingsASFIPCPort: 'ASF IPC 連接埠',
settingsASFIPCPassword: 'ASF IPC 密碼',
settingsReportIssues: '回報問題/新功能請求',
HBAlreadyOwned: '遊戲已擁有',
HBRedeemAlreadyOwned: '確定刮開 %title% Steam 序號?',
steamStore: 'Steam 商店',
HBActivationRestrictions: '啟動限制',
HBDisallowedCountries: '限制以下地區啟動',
HBExclusiveCountries: '僅限以下地區啟動',
HBCurrentLocation: '當前位於:',
DIGMenuPurchase: '購買',
DIGMenuSelectAll: '全選',
DIGMenuSelectCancel: '取消',
DIGButtonPurchasing: '購買中',
DIGInsufficientFund: '餘額不足',
DIGFinishedPurchasing: '購買完成',
DIGMarketSearchResult: '目前市集上架中',
DIGRateAllPositive: '全部好評',
DIGClickToHideThisRow: '隱藏此上架遊戲',
DIGCurrentBalance: '當前餘額:',
DIGEditBalance: '更新DIG 錢包餘額',
DIGPoint: 'DIG 點數',
DIGTotalAmount: '購買總額:',
buttonReveal: '刮開',
buttonRetrieve: '提取',
buttonActivate: '啟動',
buttonCopy: '複製',
buttonReset: '清空',
buttonExport: '匯出',
buttonCommands: '指令',
buttonLog: '日誌',
checkboxIncludeGameTitle: '遊戲名',
checkboxJoinKeys: '合併',
checkboxSkipUsed: '跳過已使用',
checkboxMarketListings: '上架於市集',
selectFilterAll: '選取全部',
selectFilterOwned: '選取已擁有',
selectFilterNotOwned: '選取未擁有',
selectConnector: '至',
markAllAsUsed: '標記全部已使用',
syncSuccessTitle: '同步成功',
syncSuccess: '成功同步Steam 遊戲庫資料',
syncFailTitle: '同步失敗',
syncFail: '失敗同步Steam 遊戲庫資料',
visitSteam: '前往Steam',
lastSyncTime: '已於%seconds% 秒前同步收藏庫',
game: '遊戲',
dlc: 'DLC',
package: '合集',
bundle: '組合包',
owned: '已擁有',
wished: '於願望清單',
ignored: '已忽略',
notOwned: '未擁有',
notApplicable: '無資料',
notFetched: '未檢查',
enablePlatiFeature: '啟用腳本',
platiFetchOnStart: '自動檢查',
platiInfiniteScroll: '自動換頁',
platiFetchButton: '檢查',
platiFilterType: '顯示類型',
platiFilterStatus: '顯示狀態',
},
schinese: {
name: '简体中文',
updateSuccessTitle: '更新成功',
updateSuccess: '成功更新Steam sessionID',
successStatus: '成功',
successTitle: '好极了!',
successDetail: '无信息',
activatedDetail: '已激活',
loadingSuccess: '加载完成!',
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',
settingsAutoSyncLibrary: '自动同步Steam 游戏库',
settingsSyncLibrary: '同步游戏库',
settingsSyncLibraryButton: '同步',
settingsLanguage: '语言',
settingsASFFormat: '启用ASF 格式',
settingsTitleComesLast: '游戏名置后',
settingsActivateAllKeys: '不跳过、激活所有激活码',
settingsEnableTooltips: 'Keylol 论坛提示窗',
settingshighlightedRegions: '标示出地区',
settingshighlightedRegionsButton: '选择地区',
settingsEnableASFIPC: '启用ASF IPC',
settingsASFWSProtocol: 'ASF WS 传输协议',
settingsASFIPCProtocol: 'ASF IPC 传输协议',
settingsASFIPCServer: 'ASF IPC IP地址',
settingsASFIPCPort: 'ASF IPC 端口',
settingsASFIPCPassword: 'ASF IPC 密码',
settingsReportIssues: '回报问题/新功能请求',
HBAlreadyOwned: '游戏已拥有',
HBRedeemAlreadyOwned: '确定刮开 %title% Steam 激活码?',
steamStore: 'Steam 商店',
HBActivationRestrictions: '激活限制',
HBDisallowedCountries: '限制以下地区激活',
HBExclusiveCountries: '仅限以下地区激活',
HBCurrentLocation: '当前位于:',
DIGMenuPurchase: '购买',
DIGMenuSelectAll: '全选',
DIGMenuSelectCancel: '取消',
DIGButtonPurchasing: '购买中',
DIGInsufficientFund: '余额不足',
DIGFinishedPurchasing: '购买完成',
DIGMarketSearchResult: '目前市集上架中',
DIGRateAllPositive: '全部好评',
DIGClickToHideThisRow: '隐藏此上架游戏',
DIGCurrentBalance: '当前余额:',
DIGEditBalance: '更新DIG 錢包餘額',
DIGPoint: 'DIG 点数',
DIGTotalAmount: '购买总额:',
buttonReveal: '刮开',
buttonRetrieve: '提取',
buttonActivate: '激活',
buttonCopy: '复制',
buttonReset: '清空',
buttonExport: '导出',
buttonCommands: '指令',
buttonLog: '日志',
checkboxIncludeGameTitle: '游戏名',
checkboxJoinKeys: '合并',
checkboxSkipUsed: '跳过已使用',
checkboxMarketListings: '上架于市集',
selectFilterAll: '选取全部',
selectFilterOwned: '选取已拥有',
selectFilterNotOwned: '选取未拥有',
selectConnector: '至',
markAllAsUsed: '标记全部已使用',
syncSuccessTitle: '同步成功',
syncSuccess: '成功同步Steam 游戏库资料',
syncFailTitle: '同步失败',
syncFail: '失败同步Steam 游戏库资料',
visitSteam: '前往Steam',
lastSyncTime: '已于%seconds% 秒前同步游戏库',
game: '游戏',
dlc: 'DLC',
package: '礼包',
bundle: '捆绑包',
owned: '已拥有',
wished: '于愿望清单',
ignored: '已忽略',
notOwned: '未拥有',
notApplicable: '无资料',
notFetched: '未检查',
enablePlatiFeature: '启用脚本',
platiFetchOnStart: '自动检查',
platiInfiniteScroll: '自动换页',
platiFetchButton: '检查',
platiFilterType: '显示类型',
platiFilterStatus: '显示状态',
},
english: {
name: 'English',
updateSuccessTitle: 'Update Successful!',
updateSuccess: 'Steam sessionID is successfully updated',
successStatus: 'Success',
successTitle: 'Hurray!',
successDetail: 'No Detail',
activatedDetail: 'Activated',
loadingSuccess: 'Loaded',
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',
settingsAutoSyncLibrary: 'Auto Sync Library',
settingsSyncLibrary: 'Sync Library',
settingsSyncLibraryButton: 'Sync',
settingsLanguage: 'Language',
settingsASFFormat: 'Enable ASF Format',
settingsTitleComesLast: 'Title Comes Last',
settingsActivateAllKeys: 'No skip & activate all keys',
settingsEnableTooltips: 'Tooltips from Keylol',
settingshighlightedRegions: 'Highlighted Regions',
settingshighlightedRegionsButton: 'Select Regions',
settingsEnableASFIPC: 'Enable ASF IPC',
settingsASFWSProtocol: 'ASF WS Protocol',
settingsASFIPCProtocol: 'ASF IPC Protocol',
settingsASFIPCServer: 'ASF IPC IP Address',
settingsASFIPCPort: 'ASF IPC Port',
settingsASFIPCPassword: 'ASF IPC Password',
settingsReportIssues: 'Report Issues or Request Features',
HBAlreadyOwned: 'Game Already Owned',
HBRedeemAlreadyOwned: 'Are you sure to redeem %title% Steam Key?',
steamStore: 'Steam Store',
HBActivationRestrictions: 'Activation Restrictions',
HBDisallowedCountries: 'Cannot be activated in the following regions',
HBExclusiveCountries: 'Can only be activated in the following regions',
HBCurrentLocation: 'Current Location: ',
DIGMenuPurchase: 'Purchase',
DIGMenuSelectAll: 'Select All',
DIGMenuSelectCancel: 'Cancel',
DIGButtonPurchasing: 'Purchassing',
DIGInsufficientFund: 'Insufficient fund',
DIGFinishedPurchasing: 'Finished Purchasing',
DIGMarketSearchResult: 'Currently listing in marketplace',
DIGRateAllPositive: 'Mark All Positive',
DIGClickToHideThisRow: 'Hide this game from listings',
DIGCurrentBalance: 'Current Balance: ',
DIGEditBalance: 'Edit DIG balance',
DIGPoint: 'DIG Point',
DIGTotalAmount: 'Total Amount: ',
buttonReveal: 'Reveal',
buttonRetrieve: 'Retrieve',
buttonActivate: 'Activate',
buttonCopy: 'Copy',
buttonReset: 'Reset',
buttonExport: 'Export',
buttonCommands: 'Commands',
buttonLog: 'Log',
checkboxIncludeGameTitle: 'Game Title',
checkboxJoinKeys: 'Join',
checkboxSkipUsed: 'Skip Used',
checkboxMarketListings: 'Market Listings',
selectFilterAll: 'Select All',
selectFilterOwned: 'Select Owned',
selectFilterNotOwned: 'Select Not Owned',
selectConnector: 'to',
markAllAsUsed: 'Mark All as Used',
syncSuccessTitle: 'Sync Successful',
syncSuccess: 'Successfully sync Steam library data',
syncFailTitle: 'Sync failed',
syncFail: 'Failed to sync Steam library data',
visitSteam: 'Visit Steam',
lastSyncTime: 'Library data synced %seconds% seconds ago',
game: 'Game',
dlc: 'DLC',
package: 'Package',
bundle: 'Bundle',
owned: 'Owned',
wished: 'Wishlisted',
ignored: 'Ignored',
notOwned: 'Not Owned',
notApplicable: 'Not Applicable',
notFetched: 'Not Checked',
enablePlatiFeature: 'Enable Script',
platiFetchOnStart: 'Auto Check',
platiInfiniteScroll: 'Infinite Scroll',
platiFetchButton: 'Check',
platiFilterType: 'Show Type',
platiFilterStatus: 'Show Status',
},
},
language: null,
set() {
const selectedLanguage = has.call(this.data, config.get('language')) ? config.get('language') : 'english';
this.language = this.data[selectedLanguage];
},
get(key) {
return has.call(this.language, key) ? this.language[key] : this.data.english[key];
},
init() {
this.set();
},
};
const ISO2 = {
name: {
tchinese: {
AD: '安道爾',
AE: '阿拉伯聯合大公國',
AF: '阿富汗',
AG: '安地卡及巴布達',
AI: '安圭拉',
AL: '阿爾巴尼亞',
AM: '亞美尼亞',
AO: '安哥拉',
AQ: '南極洲',
AR: '阿根廷',
AS: '美屬薩摩亞',
AT: '奧地利',
AU: '澳大利亞',
AW: '阿魯巴',
AX: '奧蘭',
AZ: '亞塞拜然',
BA: '波士尼亞與赫塞哥維納',
BB: '巴貝多',
BD: '孟加拉',
BE: '比利時',
BF: '布吉納法索',
BG: '保加利亞',
BH: '巴林',
BI: '蒲隆地',
BJ: '貝南',
BL: '聖巴泰勒米',
BM: '百慕達',
BN: '汶萊',
BO: '玻利維亞',
BQ: '波奈',
BR: '巴西',
BS: '巴哈馬',
BT: '不丹',
BV: '布威島',
BW: '波札那',
BY: '白俄羅斯',
BZ: '貝里斯',
CA: '加拿大',
CC: '科科斯(基林)群島',
CD: '剛果民主共和國',
CF: '中非共和國',
CG: '剛果共和國',
CH: '瑞士',
CI: '象牙海岸',
CK: '庫克群島',
CL: '智利',
CM: '喀麥隆',
CN: '中國',
CO: '哥倫比亞',
CR: '哥斯大黎加',
CS: '塞爾維亞與蒙特內哥羅',
CU: '古巴',
CV: '維德角',
CW: '古拉索',
CX: '聖誕島',
CY: '賽普勒斯',
CZ: '捷克',
DE: '德國',
DJ: '吉布地',
DK: '丹麥',
DM: '多米尼克',
DO: '多明尼加',
DZ: '阿爾及利亞',
EC: '厄瓜多',
EE: '愛沙尼亞',
EG: '埃及',
EH: '西撒哈拉',
ER: '厄利垂亞',
ES: '西班牙',
ET: '衣索比亞',
FI: '芬蘭',
FJ: '斐濟',
FK: '福克蘭群島',
FM: '密克羅尼西亞聯邦',
FO: '法羅群島',
FR: '法國',
GA: '加彭',
GB: '英國',
GD: '格瑞那達',
GE: '喬治亞',
GF: '法屬圭亞那',
GG: '根西',
GH: '迦納',
GI: '直布羅陀',
GL: '格陵蘭',
GM: '甘比亞',
GN: '幾內亞',
GP: '瓜德羅普',
GQ: '赤道幾內亞',
GR: '希臘',
GS: '南喬治亞與南桑威奇',
GT: '瓜地馬拉',
GU: '關島',
GW: '幾內亞比索',
GY: '蓋亞那',
HK: '香港',
HM: '赫德島和麥克唐納群島',
HN: '宏都拉斯',
HR: '克羅埃西亞',
HT: '海地',
HU: '匈牙利',
ID: '印尼',
IE: '愛爾蘭',
IL: '以色列',
IM: '曼島',
IN: '印度',
IO: '英屬印度洋領地',
IQ: '伊拉克',
IR: '伊朗',
IS: '冰島',
IT: '義大利',
JE: '澤西',
JM: '牙買加',
JO: '約旦',
JP: '日本',
KE: '肯亞',
KG: '吉爾吉斯',
KH: '柬埔寨',
KI: '吉里巴斯',
KM: '葛摩',
KN: '聖克里斯多福及尼維斯',
KP: '北韓',
KR: '南韓',
KW: '科威特',
KY: '開曼群島',
KZ: '哈薩克',
LA: '寮國',
LB: '黎巴嫩',
LC: '聖露西亞',
LI: '列支敦斯登',
LK: '斯里蘭卡',
LR: '賴比瑞亞',
LS: '賴索托',
LT: '立陶宛',
LU: '盧森堡',
LV: '拉脫維亞',
LY: '利比亞',
MA: '摩洛哥',
MC: '摩納哥',
MD: '摩爾多瓦',
ME: '蒙特內哥羅',
MF: '法屬聖馬丁',
MG: '馬達加斯加',
MH: '馬紹爾群島',
MK: '馬其頓共和國',
ML: '馬利',
MM: '緬甸',
MN: '蒙古',
MO: '澳門',
MP: '北馬里亞納群島',
MQ: '馬丁尼克',
MR: '茅利塔尼亞',
MS: '蒙哲臘',
MT: '馬爾他',
MU: '模里西斯',
MV: '馬爾地夫',
MW: '馬拉威',
MX: '墨西哥',
MY: '馬來西亞',
MZ: '莫三比克',
NA: '納米比亞',
NC: '新喀里多尼亞',
NE: '尼日',
NF: '諾福克島',
NG: '奈及利亞',
NI: '尼加拉瓜',
NL: '荷蘭',
NO: '挪威',
NP: '尼泊爾',
NR: '諾魯',
NU: '紐埃',
NZ: '紐西蘭',
OM: '阿曼',
PA: '巴拿馬',
PE: '秘魯',
PF: '法屬玻里尼西亞',
PG: '巴布亞紐幾內亞',
PH: '菲律賓',
PK: '巴基斯坦',
PL: '波瀾',
PM: '聖皮耶與密克隆群島',
PN: '皮特肯群島',
PR: '波多黎各',
PS: '巴勒斯坦',
PT: '葡萄牙',
PW: '帛琉',
PY: '巴拉圭',
QA: '卡達',
RE: '留尼旺',
RO: '羅馬尼亞',
RS: '塞爾維亞',
RU: '俄羅斯',
RW: '盧安達',
SA: '沙烏地阿拉伯',
SB: '索羅門群島',
SC: '塞席爾',
SD: '蘇丹',
SE: '瑞典',
SG: '新加坡',
SH: '聖赫勒拿、亞森欣與垂斯坦昆哈',
SI: '斯洛維尼亞',
SJ: '斯瓦巴和揚馬延',
SK: '斯洛伐克',
SL: '獅子山共和國',
SM: '聖馬利諾',
SN: '塞內加爾',
SO: '索馬利亞',
SR: '蘇利南',
SS: '南蘇丹',
ST: '聖多美普林西比',
SV: '薩爾瓦多',
SX: '荷屬聖馬丁',
SY: '敘利亞',
SZ: '史瓦濟蘭',
TC: '土克凱可群島',
TD: '查德',
TF: '法屬南部和南極領地',
TG: '多哥',
TH: '泰國',
TJ: '塔吉克',
TK: '托克勞',
TL: '東帝汶',
TM: '土庫曼',
TN: '突尼西亞',
TO: '東加',
TR: '土耳其',
TT: '千里達及托巴哥',
TV: '吐瓦魯',
TW: '臺灣',
TZ: '坦尚尼亞',
UA: '烏克蘭',
UG: '烏干達',
UM: '美國本土外小島嶼',
US: '美國',
UY: '烏拉圭',
UZ: '烏茲別克',
VA: '聖座',
VC: '聖文森及格瑞那丁',
VE: '委內瑞拉',
VG: '英屬維京群島',
VI: '美屬維京群島',
VN: '越南',
VU: '萬那杜',
WF: '瓦利斯和富圖納',
WS: '薩摩亞',
XK: '科索沃',
YE: '葉門',
YT: '馬約特',
ZA: '南非',
ZM: '尚比亞',
ZW: '辛巴威',
},
schinese: {
AD: '安道尔',
AE: '阿拉伯联合酋长国',
AF: '阿富汗',
AG: '安提瓜和巴布达',
AI: '安圭拉',
AL: '阿尔巴尼亚',
AM: '亚美尼亚',
AO: '安哥拉',
AQ: '南极洲',
AR: '阿根廷',
AS: '美属萨摩亚',
AT: '奥地利',
AU: '澳大利亚',
AW: '阿鲁巴',
AX: '奥兰群岛',
AZ: '阿塞拜疆',
BA: '波斯尼亚和黑塞哥维那',
BB: '巴巴多斯',
BD: '孟加拉',
BE: '比利时',
BF: '布基纳法索',
BG: '保加利亚',
BH: '巴林',
BI: '布隆迪',
BJ: '贝宁',
BL: '圣巴托洛缪岛',
BM: '百慕大',
BN: '文莱',
BO: '玻利维亚',
BQ: '博奈尔',
BR: '巴西',
BS: '巴哈马',
BT: '不丹',
BV: '布韦岛',
BW: '博兹瓦纳',
BY: '白俄罗斯',
BZ: '伯利兹',
CA: '加拿大',
CC: '科科斯(基林)群岛',
CD: '刚果(金)',
CF: '中非共和国',
CG: '刚果(布)',
CH: '瑞士',
CI: '科特迪瓦',
CK: '库克群岛',
CL: '智利',
CM: '喀麦隆',
CN: '中国',
CO: '哥伦比亚',
CR: '哥斯达黎加',
CS: '塞尔维亚和黑山',
CU: '古巴',
CV: '佛得角',
CW: '库拉索',
CX: '圣诞岛',
CY: '塞浦路斯',
CZ: '捷克',
DE: '德国',
DJ: '吉布提',
DK: '丹麦',
DM: '多米尼克',
DO: '多米尼加',
DZ: '阿尔及利亚',
EC: '厄瓜多尔',
EE: '爱沙尼亚',
EG: '埃及',
EH: '西撒哈拉',
ER: '厄立特里亚',
ES: '西班牙',
ET: '埃塞俄比亚',
FI: '芬兰',
FJ: '斐济',
FK: '福克兰群岛',
FM: '密克罗尼西亚',
FO: '法罗群岛',
FR: '法国',
GA: '加蓬',
GB: '英国',
GD: '格林纳达',
GE: '格鲁吉亚',
GF: '法属圭亚那',
GG: '根西',
GH: '加纳',
GI: '直布罗陀',
GL: '格陵兰',
GM: '冈比亚',
GN: '几内亚',
GP: '瓜德鲁普',
GQ: '赤道几内亚',
GR: '希腊',
GS: '南乔治亚岛和南桑威奇群岛',
GT: '危地马拉',
GU: '关岛',
GW: '几内亚比绍',
GY: '圭亚那',
HK: '香港',
HM: '赫德岛和麦克唐纳群岛',
HN: '洪都拉斯',
HR: '克罗地亚',
HT: '海地',
HU: '匈牙利',
ID: '印尼',
IE: '爱尔兰',
IL: '以色列',
IM: '马恩岛',
IN: '印度',
IO: '英属印度洋领地',
IQ: '伊拉克',
IR: '伊朗',
IS: '冰岛',
IT: '意大利',
JE: '泽西岛',
JM: '牙买加',
JO: '约旦',
JP: '日本',
KE: '肯尼亚',
KG: '吉尔吉斯',
KH: '柬埔寨',
KI: '基里巴斯',
KM: '科摩罗',
KN: '圣基茨和尼维斯',
KP: '朝鲜',
KR: '韩国',
KW: '科威特',
KY: '开曼群岛',
KZ: '哈萨克斯坦',
LA: '老挝',
LB: '黎巴嫩',
LC: '圣卢西亚',
LI: '列支敦士登',
LK: '斯里兰卡',
LR: '利比里亚',
LS: '莱索托',
LT: '立陶宛',
LU: '卢森堡',
LV: '拉脱维亚',
LY: '利比亚',
MA: '摩洛哥',
MC: '摩纳哥',
MD: '摩尔多瓦',
ME: '黑山',
MF: '法属圣马丁',
MG: '马达加斯加',
MH: '马绍尔群岛',
MK: '马其顿',
ML: '马里',
MM: '缅甸',
MN: '蒙古',
MO: '澳门',
MP: '北马里亚纳群岛',
MQ: '马提尼克',
MR: '毛里塔尼亚',
MS: '蒙塞拉特',
MT: '马耳他',
MU: '毛里求斯',
MV: '马尔代夫',
MW: '马拉维',
MX: '墨西哥',
MY: '马来西亚',
MZ: '莫桑比克',
NA: '纳米比亚',
NC: '新喀里多尼亚',
NE: '尼日尔',
NF: '诺福克岛',
NG: '尼日利',
NI: '尼加拉瓜',
NL: '荷兰',
NO: '挪威',
NP: '尼泊尔',
NR: '瑙鲁',
NU: '纽埃',
NZ: '新西兰',
OM: '阿曼',
PA: '巴拿马',
PE: '秘鲁',
PF: '法属波利尼西亚a',
PG: '巴布亚新几内亚',
PH: '菲律宾',
PK: '巴基斯坦',
PL: '波兰',
PM: '圣皮埃尔和密克隆',
PN: '皮特凯恩群岛',
PR: '波多黎各',
PS: '巴勒斯坦',
PT: '葡萄牙',
PW: '帕劳',
PY: '巴拉圭',
QA: '卡塔尔',
RE: '留尼旺島',
RO: '罗马尼亚',
RS: '塞尔维亚',
RU: '俄罗斯',
RW: '卢旺达',
SA: '沙特阿拉伯',
SB: '所罗门群岛',
SC: '塞舌尔',
SD: '苏丹',
SE: '瑞典',
SG: '新加坡',
SH: '圣赫勒拿、阿森松与特斯坦达库尼亚',
SI: '斯洛文尼',
SJ: '斯瓦尔巴群岛和扬马延岛',
SK: '斯洛伐克',
SL: '塞拉利昂',
SM: '圣马力诺',
SN: '塞内加尔',
SO: '索马里',
SR: '苏里南',
SS: '南苏丹',
ST: '圣多美和普林西比',
SV: '萨尔瓦多',
SX: '荷属圣马丁',
SY: '叙利亚',
SZ: '斯威士兰',
TC: '特克斯和凯科斯群岛',
TD: '乍得',
TF: '法属南部领土',
TG: '多哥',
TH: '泰国',
TJ: '塔吉克斯坦',
TK: '托克劳',
TL: '东帝汶',
TM: '土库曼斯坦',
TN: '突尼斯',
TO: '汤加',
TR: '土耳其',
TT: '特立尼达和多巴哥',
TV: '图瓦卢',
TW: '台湾',
TZ: '坦桑尼亚',
UA: '乌克兰',
UG: '乌干达',
UM: '美国本土外小岛屿',
US: '美国',
UY: '乌拉圭',
UZ: '乌兹别克斯坦',
VA: '圣座',
VC: '圣文森特和格林纳丁斯',
VE: '委内瑞拉',
VG: '英属维尔京群岛',
VI: '美属维尔京群岛',
VN: '越南',
VU: '瓦努阿图',
WF: '瓦利斯和富图纳群岛',
WS: '萨摩亚',
XK: '科索沃',
YE: '也门',
YT: '马约特',
ZA: '南非',
ZM: '赞比亚',
ZW: '津巴布韦',
},
english: {
AD: 'Andorra',
AE: 'United Arab Emirates',
AF: 'Afghanistan',
AG: 'Antigua and Barbuda',
AI: 'Anguilla',
AL: 'Albania',
AM: 'Armenia',
AO: 'Angola',
AQ: 'Antarctica',
AR: 'Argentina',
AS: 'American Samoa',
AT: 'Austria',
AU: 'Australia',
AW: 'Aruba',
AX: 'Aland Islands',
AZ: 'Azerbaijan',
BA: 'Bosnia and Herzegovina',
BB: 'Barbados',
BD: 'Bangladesh',
BE: 'Belgium',
BF: 'Burkina Faso',
BG: 'Bulgaria',
BH: 'Bahrain',
BI: 'Burundi',
BJ: 'Benin',
BL: 'Saint Barthélemy',
BM: 'Bermuda',
BN: 'Brunei',
BO: 'Bolivia',
BQ: 'Bonaire',
BR: 'Brazil',
BS: 'Bahamas',
BT: 'Bhutan',
BV: 'Bouvet Island',
BW: 'Botswana',
BY: 'Belarus',
BZ: 'Belize',
CA: 'Canada',
CC: 'Cocos (Keeling) Islands',
CD: 'East Congo',
CF: 'Central African Republic',
CG: 'West Congo',
CH: 'Switzerland',
CI: 'Ivory Coast',
CK: 'Cook Islands',
CL: 'Chile',
CM: 'Cameroon',
CN: 'China',
CO: 'Colombia',
CR: 'Costa Rica',
CS: 'Serbia and Montenegro',
CU: 'Cuba',
CV: 'Cabo Verde',
CW: 'Curaçao',
CX: 'Christmas Island',
CY: 'Cyprus',
CZ: 'Czechia',
DE: 'Germany',
DJ: 'Djibouti',
DK: 'Denmark',
DM: 'Dominica',
DO: 'Dominican Republic',
DZ: 'Algeria',
EC: 'Ecuador',
EE: 'Estonia',
EG: 'Egypt',
EH: 'Western Sahara',
ER: 'Eritrea',
ES: 'Spain',
ET: 'Ethiopia',
FI: 'Finland',
FJ: 'Fiji',
FK: 'Falkland Islands',
FM: 'Micronesia',
FO: 'Faroe Islands',
FR: 'France',
GA: 'Gabon',
GB: 'United Kingdom',
GD: 'Grenada',
GE: 'Georgia',
GF: 'French Guiana',
GG: 'Guernsey',
GH: 'Ghana',
GI: 'Gibraltar',
GL: 'Greenland',
GM: 'Gambia',
GN: 'Guinea',
GP: 'Guadeloupe',
GQ: 'Equatorial Guinea',
GR: 'Greece',
GS: 'South Georgia and the South Sandwich Islands',
GT: 'Guatemala',
GU: 'Guam',
GW: 'Guinea-Bissau',
GY: 'Guyana',
HK: 'Hong Kong',
HM: 'Heard Island and McDonald Islands',
HN: 'Honduras',
HR: 'Croatia',
HT: 'Haiti',
HU: 'Hungary',
ID: 'Indonesia',
IE: 'Ireland',
IL: 'Israel',
IM: 'Isle of Man',
IN: 'India',
IO: 'British Indian Ocean Territory',
IQ: 'Iraq',
IR: 'Iran',
IS: 'Iceland',
IT: 'Italy',
JE: 'Jersey',
JM: 'Jamaica',
JO: 'Jordan',
JP: 'Japan',
KE: 'Kenya',
KG: 'Kyrgyzstan',
KH: 'Cambodia',
KI: 'Kiribati',
KM: 'Comoros',
KN: 'Saint Kitts and Nevis',
KP: 'North Korea',
KR: 'South Korea',
KW: 'Kuwait',
KY: 'Cayman Islands',
KZ: 'Kazakhstan',
LA: 'Lao',
LB: 'Lebanon',
LC: 'Saint Lucia',
LI: 'Liechtenstein',
LK: 'Sri Lanka',
LR: 'Liberia',
LS: 'Lesotho',
LT: 'Lithuania',
LU: 'Luxembourg',
LV: 'Latvia',
LY: 'Libya',
MA: 'Morocco',
MC: 'Monaco',
MD: 'Moldova',
ME: 'Montenegro',
MF: 'Saint Martin (French part)',
MG: 'Madagascar',
MH: 'Marshall Islands',
MK: 'Macedonia',
ML: 'Mali',
MM: 'Myanmar',
MN: 'Mongolia',
MO: 'Macao',
MP: 'Northern Mariana Islands',
MQ: 'Martinique',
MR: 'Mauritania',
MS: 'Montserrat',
MT: 'Malta',
MU: 'Mauritius',
MV: 'Maldives',
MW: 'Malawi',
MX: 'Mexico',
MY: 'Malaysia',
MZ: 'Mozambique',
NA: 'Namibia',
NC: 'New Caledonia',
NE: 'Niger',
NF: 'Norfolk Island',
NG: 'Nigeria',
NI: 'Nicaragua',
NL: 'Netherlands',
NO: 'Norway',
NP: 'Nepal',
NR: 'Nauru',
NU: 'Niue',
NZ: 'New Zealand',
OM: 'Oman',
PA: 'Panama',
PE: 'Peru',
PF: 'French Polynesia',
PG: 'Papua New Guinea',
PH: 'Philippines',
PK: 'Pakistan',
PL: 'Poland',
PM: 'Saint Pierre and Miquelon',
PN: 'Pitcairn',
PR: 'Puerto Rico',
PS: 'Palestine',
PT: 'Portugal',
PW: 'Palau',
PY: 'Paraguay',
QA: 'Qatar',
RE: 'Reunion',
RO: 'Romania',
RS: 'Serbia',
RU: 'Russia',
RW: 'Rwanda',
SA: 'Saudi Arabia',
SB: 'Solomon Islands',
SC: 'Seychelles',
SD: 'Sudan',
SE: 'Sweden',
SG: 'Singapore',
SH: 'Saint Helena, Ascension and Tristan da Cunha',
SI: 'Slovenia',
SJ: 'Svalbard and Jan Mayen',
SK: 'Slovakia',
SL: 'Sierra Leone',
SM: 'San Marino',
SN: 'Senegal',
SO: 'Somalia',
SR: 'Suriname',
SS: 'South Sudan',
ST: 'Sao Tome and Principe',
SV: 'El Salvador',
SX: 'Sint Maarten (Dutch part)',
SY: 'Syria',
SZ: 'Swaziland',
TC: 'Turks and Caicos Islands',
TD: 'Chad',
TF: 'French Southern Territories',
TG: 'Togo',
TH: 'Thailand',
TJ: 'Tajikistan',
TK: 'Tokelau',
TL: 'Timor-Leste',
TM: 'Turkmenistan',
TN: 'Tunisia',
TO: 'Tonga',
TR: 'Turkey',
TT: 'Trinidad and Tobago',
TV: 'Tuvalu',
TW: 'Taiwan',
TZ: 'Tanzania',
UA: 'Ukraine',
UG: 'Uganda',
UM: 'United States Minor Outlying Islands',
US: 'United States',
UY: 'Uruguay',
UZ: 'Uzbekistan',
VA: 'Holy See',
VC: 'Saint Vincent and the Grenadines',
VE: 'Venezuela',
VG: 'Virgin Islands, British',
VI: 'Virgin Islands, U.S.',
VN: 'Viet Nam',
VU: 'Vanuatu',
WF: 'Wallis and Futuna',
WS: 'Samoa',
XK: 'Kosovo',
YE: 'Yemen',
YT: 'Mayotte',
ZA: 'South Africa',
ZM: 'Zambia',
ZW: 'Zimbabwe',
},
},
get(code, language) {
const data = this.name[(language || config.get('language') || 'english')];
return has.call(data, code) ? data[code] : code;
},
};
const xe = {
exchangeRate: JSON.parse(GM_getValue('SBSE_xe', '{}')),
currencies: {
AUD: {
english: 'Australian Dollar',
tchinese: '澳幣',
schinese: '澳元',
symbol: 'AU$',
decimal: true,
},
CAD: {
english: 'Canadian Dollar',
tchinese: '加幣',
schinese: '加元',
symbol: 'CA$',
decimal: true,
},
CNY: {
english: 'Chinese Yuan',
tchinese: '人民幣',
schinese: '人民币',
symbol: 'CN¥',
decimal: true,
},
EUR: {
english: 'Euro',
tchinese: '歐元',
schinese: '欧元',
symbol: '€',
decimal: true,
},
GBP: {
english: 'Great Britain Pound',
tchinese: '英鎊',
schinese: '英镑',
symbol: '£',
decimal: true,
},
HKD: {
english: 'Hong Kong Dollar',
tchinese: '港幣',
schinese: '港元',
symbol: 'HK$',
decimal: false,
},
JPY: {
english: 'Japanese Yen',
tchinese: '日圓',
schinese: '日元',
symbol: 'JP¥',
decimal: false,
},
KRW: {
english: 'South Korean Won',
tchinese: '韓圓',
schinese: '韩币',
symbol: '₩',
decimal: false,
},
MYR: {
english: 'Malaysian Ringgit',
tchinese: '令吉',
schinese: '林吉特',
symbol: 'RM',
decimal: true,
},
NTD: {
english: 'New Taiwan Dollar',
tchinese: '台幣',
schinese: '台币',
symbol: 'NT$',
decimal: false,
},
NZD: {
english: 'New Zealand Dollar',
tchinese: '紐幣',
schinese: '新西兰元',
symbol: 'NZ$',
decimal: true,
},
RUB: {
english: 'Russian Ruble',
tchinese: '盧布',
schinese: '卢布',
symbol: '₽',
decimal: false,
},
USD: {
english: 'United States Dollar',
tchinese: '美元',
schinese: '美元',
symbol: 'US$',
decimal: true,
},
},
getRate() {
const self = this;
GM_xmlhttpRequest({
method: 'GET',
url: 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml',
onload: (res) => {
if (res.status === 200) {
try {
const exchangeRate = {
lastUpdate: Date.now(),
rates: {},
};
res.response.split(eol).forEach((line) => {
if (line.includes('currency=')) {
const currency = line.split('currency=\'').pop().slice(0, 3);
const rate = line.trim().split('rate=\'').pop().slice(0, -3);
exchangeRate.rates[currency] = parseFloat(rate);
}
});
exchangeRate.rates.EUR = 1;
// get NTD
GM_xmlhttpRequest({
method: 'GET',
url: 'https://www.google.com/search?q=1+EUR+%3D+NTD',
onload: (searchRes) => {
const rate = parseFloat(searchRes.response.split('<div class="vk_ans vk_bk">').pop().slice(0, 7).trim());
const NTDRate = isNaN(rate) ? exchangeRate.rates.HKD * 3.75 : rate;
exchangeRate.rates.NTD = NTDRate;
self.exchangeRate = exchangeRate;
GM_setValue('SBSE_xe', JSON.stringify(exchangeRate));
},
});
// get UAH
GM_xmlhttpRequest({
method: 'GET',
url: 'https://www.google.com/search?q=1+EUR+%3D+UAH',
onload: (searchRes) => {
const rate = parseFloat(searchRes.response.split('<div class="vk_ans vk_bk">').pop().slice(0, 7).trim());
const UAHRate = isNaN(rate) ? 32.85 : rate;
exchangeRate.rates.UAH = UAHRate;
self.exchangeRate = exchangeRate;
GM_setValue('SBSE_xe', JSON.stringify(exchangeRate));
},
});
} catch (e) {
swal(
'Parsing Failed',
'An error occured when parsing exchange rate data, please reload to try again',
'error',
);
}
} else {
swal(
'Loading Failed',
'Unable to fetch exchange rate data, please reload to try again',
'error',
);
}
},
});
},
update(targetCurrency = 'USD') {
$('.SBSE-price').each((i, ele) => {
const originalCurrency = ele.dataset.currency;
const originalValue = parseInt(ele.dataset.value, 10);
const originalRate = this.exchangeRate.rates[originalCurrency];
const targetRate = this.exchangeRate.rates[targetCurrency];
const exchangedValue = Math.trunc((originalValue / originalRate) * targetRate);
const symbol = this.currencies[targetCurrency].symbol;
const decimalPlace = this.currencies[targetCurrency].decimal ? 2 : 0;
$(ele).text(symbol + (exchangedValue / 100).toFixed(decimalPlace));
});
GM_setValue('SBSE_selectedCurrency', targetCurrency);
},
init() {
const updateTimer = 12 * 60 * 60 * 1000; // update every 12 hours
const newRate = ['UAH'];
if (Object.keys(this.exchangeRate).length === 0 ||
this.exchangeRate.lastUpdate < (Date.now() - updateTimer) ||
newRate.filter(x => !has.call(this.exchangeRate.rates, x)).length > 0
) this.getRate();
},
};
const steam = {
library: JSON.parse(GM_getValue('SBSE_steam_library', '{}')),
games: JSON.parse(GM_getValue('SBSE_steam_games', '{}')),
getSessionID: async () => {
const res = await request({
method: 'GET',
url: 'https://store.steampowered.com/',
});
if (res.status === 200) {
const accountID = res.response.match(/g_AccountID = (\d+)/).pop();
const sessionID = res.response.match(/g_sessionID = "(\w+)"/).pop();
if (accountID > 0) config.set('sessionID', sessionID);
else {
swal({
title: i18n.get('notLoggedInTitle'),
text: i18n.get('notLoggedInMsg'),
type: 'error',
showCancelButton: true,
}).then((result) => {
if (result.value === true) window.open('https://store.steampowered.com/');
});
}
}
},
sync(a = []) {
if (!isArray(a) || a.length === 0) {
a.push({
key: 'library',
sync: true,
save: true
}, {
key: 'games',
sync: true,
save: true
}, );
}
const self = this;
a.forEach((o) => {
if (o.key === 'library' && o.sync !== false) {
if (o.notify === true) swal.showLoading();
GM_xmlhttpRequest({
method: 'GET',
url: `https://store.steampowered.com/dynamicstore/userdata/t=${Math.random()}`,
onload(res) {
const data = JSON.parse(res.response);
if (!isObject(self.library)) self.reset([o]);
self.library.owned = {
app: isArray(data.rgOwnedApps) ? data.rgOwnedApps : [],
sub: isArray(data.rgOwnedPackages) ? data.rgOwnedPackages : [],
};
self.library.wished = {
app: isArray(data.rgWishlist) ? data.rgWishlist : [],
sub: [],
};
self.library.ignored = {
app: isArray(data.rgIgnoredApps) ? data.rgIgnoredApps : [],
sub: isArray(data.rgIgnoredPackages) ? data.rgIgnoredPackages : [],
};
self.library.lastSync = Date.now();
self.save([o]);
if (o.notify === true) {
swal({
title: i18n.get('syncSuccessTitle'),
text: i18n.get('syncSuccess'),
type: 'success',
timer: 3000,
});
}
if (typeof o.callback === 'function') o.callback();
},
onerror() {
swal({
title: i18n.get('syncFailTitle'),
text: i18n.get('syncFail'),
type: 'error',
confirmButtonText: i18n.get('visitSteam'),
showCancelButton: true,
}).then((result) => {
if (result.value === true) window.open('https://store.steampowered.com/');
});
},
});
}
if (o.key === 'games' && o.sync !== false) {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://steamspy.com/api.php?request=all',
onload(res) {
try {
const data = JSON.parse(res.response);
self.games = {
list: Object.keys(data).map(x => parseInt(x, 10)),
lastSync: Date.now(),
};
self.save([o]);
if (typeof o.callback === 'function') o.callback();
} catch (e) {
throw e.stack;
}
},
});
}
});
},
reset(a = []) {
if (!isArray(a) || a.length === 0) {
a.push({
key: 'library',
reset: true,
save: true
}, {
key: 'games',
reset: true,
save: true
}, );
}
a.forEach((o) => {
if (o.key === 'library' && o.reset !== false) {
this.library = {
lastSync: 0,
owned: {
app: [],
sub: []
},
wished: {
app: [],
sub: []
},
ignored: {
app: [],
sub: []
},
};
}
if (o.key === 'games' && o.reset !== false) {
this.games = {
lastSync: 0,
list: [],
};
}
if (o.save !== false) this.save([o]);
});
},
save(a = []) {
if (!isArray(a) || a.length === 0) {
a.push({
key: 'library',
save: true
}, {
key: 'games',
save: true
}, );
}
a.forEach((o) => {
if (has.call(this, o.key) && o.save !== false) {
GM_setValue(`SBSE_steam_${o.key}`, JSON.stringify(this[o.key]));
if (typeof o.callback === 'function') o.callback();
}
});
},
lastSync(key) {
return has.call(this, key) ? this[key].lastSync : null;
},
isOwned(o) {
return this.library.owned.app.includes(o.app) || this.library.owned.sub.includes(o.sub);
},
isWished(o) {
return this.library.wished.app.includes(o.app) || this.library.wished.sub.includes(o.sub);
},
isIgnored(o) {
return this.library.ignored.app.includes(o.app) || this.library.ignored.sub.includes(o.sub);
},
isGame(o) {
return this.games.list.length > 0 && this.games.list.includes(o.app);
},
isDLC(o) {
return has.call(o, 'app') && this.games.list.length > 0 && !this.games.list.includes(o.app);
},
isPackage(o) {
return has.call(o, 'sub');
},
init() {
if (!isObject(this.library) ||
!has.call(this.library, 'owned') ||
!has.call(this.library, 'wished') ||
!has.call(this.library, 'ignored')) this.reset([{
key: 'library'
}]);
if (!isObject(this.games) ||
!has.call(this.games, 'list')) this.reset([{
key: 'games'
}]);
if (config.get('autoSyncLibrary')) {
// sync Steam library every 10 min
const libraryTimer = 10 * 60 * 1000;
const libraryLastSync = this.lastSync('library');
if (!libraryLastSync || libraryLastSync < (Date.now() - libraryTimer)) this.sync([{
key: 'library'
}]);
// sync Steam games list every day
const gamesTimer = 1 * 24 * 60 * 60 * 1000;
const gamesLastSync = this.lastSync('games');
if (!gamesLastSync || gamesLastSync < (Date.now() - gamesTimer) || this.games.list.length === 0) this.sync([{
key: 'games'
}]);
}
// delete odd values
GM_deleteValue('SBSE_steam');
},
};
const activator = {
activated: JSON.parse(GM_getValue('SBSE_activated', '[]')),
isActivated(key) {
return this.activated.includes(key);
},
pushActivated(key) {
this.activated.push(key);
GM_setValue('SBSE_activated', JSON.stringify(this.activated));
},
keyDetails: {},
isOwned(key) {
return has.call(this.keyDetails, key) ? this.keyDetails[key].owned : false;
},
pushKeyDetails(data) {
if (!has.call(this.keyDetails, data.key)) this.keyDetails[data.key] = data;
},
getKeyDetails(key) {
return has.call(this.keyDetails, key) ? this.keyDetails[key] : null;
},
results: [],
resultDetails(result) {
// result from Steam
if (result.SBSE !== true) {
// get status
let status = i18n.get('failStatus');
let statusMsg = i18n.get('failDetailUnexpected');
const errorCode = result.purchase_result_details;
const errors = {
14: i18n.get('failDetailInvalidKey'),
15: i18n.get('failDetailUsedKey'),
53: i18n.get('failDetailRateLimited'),
13: i18n.get('failDetailCountryRestricted'),
9: i18n.get('failDetailAlreadyOwned'),
24: i18n.get('failDetailMissingBaseGame'),
36: i18n.get('failDetailPS3Required'),
50: i18n.get('failDetailGiftWallet'),
};
if (result.success === 1) {
status = i18n.get('successStatus');
statusMsg = i18n.get('successDetail');
} else if (result.success === 2) {
if (has.call(errors, errorCode)) statusMsg = errors[errorCode];
}
result.status = `${status}/${statusMsg}`;
// get description
const info = result.purchase_receipt_info;
const chuncks = [];
if (info && info.line_items) {
info.line_items.forEach((item) => {
const chunk = [];
if (item.packageid > 0) chunk.push(`sub: ${item.packageid}`);
if (item.appid > 0) chunk.push(`app: ${item.appid}`);
chunk.push(item.line_item_description);
chuncks.push(chunk.join(' '));
});
}
result.descripton = chuncks.join(', ');
}
const temp = [result.key];
if (result.status) temp.push(result.status);
if (result.descripton) temp.push(result.descripton);
return temp.join(' | ');
},
activate(keys, callback) {
this.results.length = 0;
const updateResults = () => {
$('.SBSE-container__content__model[data-feature="SBSE"] > textarea').val(this.results.concat(keys).join(eol));
};
const activateHandler = () => {
const key = keys.shift();
if (key) {
if (this.isActivated(key)) {
this.results.push(this.resultDetails({
SBSE: true,
key,
status: `${i18n.get('skippedStatus')}/${i18n.get('activatedDetail')}`,
descripton: i18n.get('noItemDetails'),
}));
updateResults();
// next key
activateHandler();
} else if (this.isOwned(key) && !config.get('activateAllKeys')) {
const detail = this.getKeyDetails(key);
const description = [];
['app', 'sub'].forEach((type) => {
if (has.call(detail, type)) description.push(`${type}: ${detail[type]} ${detail.title}`);
});
this.results.push(this.resultDetails({
SBSE: true,
key,
status: `${i18n.get('skippedStatus')}/${i18n.get('failDetailAlreadyOwned')}`,
descripton: description.join(),
}));
updateResults();
// next key
activateHandler();
} else {
const self = this;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://store.steampowered.com/account/ajaxregisterkey/',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Origin: 'https://store.steampowered.com',
Referer: 'https://store.steampowered.com/account/registerkey',
},
data: `product_key=${key}&sessionid=${config.get('sessionID')}`,
onload(res) {
if (res.status === 200) {
const result = JSON.parse(res.response);
// update activated
const failCode = result.purchase_result_details;
if (result.success === 1 || [14, 15, 9].includes(failCode)) {
self.pushActivated(key);
// dispatch activated event
$(document).trigger('activated', [key, result]);
}
result.key = key;
self.results.push(self.resultDetails(result));
updateResults();
// next key
setTimeout(activateHandler.bind(self), 2000);
} else {
const errorMsg = [];
errorMsg.push('<pre class="SBSE-errorMsg">');
errorMsg.push(`sessionID: ${config.get('sessionID') + eol}`);
errorMsg.push(`autoUpdate: ${config.get('autoUpdateSessionID') + eol}`);
errorMsg.push(`status: ${res.status + eol}`);
errorMsg.push(`response: ${res.response + eol}`);
errorMsg.push('</pre>');
swal({
title: i18n.get('failTitle'),
html: i18n.get('failDetailRequestFailedNeedUpdate') + eol + errorMsg.join(''),
type: 'error',
});
steam.getSessionID();
if (typeof callback === 'function') callback();
}
},
});
}
} else if (typeof callback === 'function') callback();
};
activateHandler();
},
};
// models
const settings = {
model: null,
getModel() {
return this.model instanceof $ ? this.model : $();
},
display() {
swal({
title: i18n.get('settingsTitle'),
onBeforeOpen: (dom) => {
$(dom).find('.swal2-content').append(settings.getModel());
},
});
},
init() {
const settingDetails = [{
name: i18n.get('settingsAutoUpdateSessionID'),
configItem: 'autoUpdateSessionID',
type: 'switch',
}, {
name: i18n.get('settingsSessionID'),
configItem: 'sessionID',
type: 'text',
}, {
name: i18n.get('settingsAutoSyncLibrary'),
configItem: 'autoSyncLibrary',
type: 'switch',
}, {
name: i18n.get('settingsSyncLibrary'),
configItem: 'syncLibrary',
type: 'button',
textContent: i18n.get('settingsSyncLibraryButton'),
}, {
name: i18n.get('settingsLanguage'),
configItem: 'language',
type: 'select',
}, {
name: i18n.get('settingsASFFormat'),
configItem: 'ASFFormat',
type: 'switch',
}, {
name: i18n.get('settingsTitleComesLast'),
configItem: 'titleComesLast',
type: 'switch',
}, {
name: i18n.get('settingsActivateAllKeys'),
configItem: 'activateAllKeys',
type: 'switch',
}, {
name: i18n.get('settingsEnableTooltips'),
configItem: 'enableTooltips',
type: 'switch',
}, {
name: i18n.get('settingshighlightedRegions'),
configItem: 'highlightedRegions',
type: 'button',
textContent: i18n.get('settingshighlightedRegionsButton'),
}, {
name: i18n.get('settingsEnableASFIPC'),
configItem: 'enableASFIPC',
type: 'switch',
}, {
name: i18n.get('settingsASFWSProtocol'),
configItem: 'ASFWSProtocol',
type: 'select',
options: ['ws', 'wss'],
}, {
name: i18n.get('settingsASFIPCProtocol'),
configItem: 'ASFIPCProtocol',
type: 'select',
options: ['http', 'https'],
}, {
name: i18n.get('settingsASFIPCServer'),
configItem: 'ASFIPCServer',
type: 'text',
}, {
name: i18n.get('settingsASFIPCPort'),
configItem: 'ASFIPCPort',
type: 'text',
}, {
name: i18n.get('settingsASFIPCPassword'),
configItem: 'ASFIPCPassword',
type: 'text',
}];
const $model = $('<div class="SBSE-container__content__model" data-feature="setting"><table></table></div>');
// append settings
settingDetails.forEach((detail) => {
const $tr = $(`<tr><td class="name">${detail.name}</td><td class="value"></td></tr>`).appendTo($model.find('table'));
switch (detail.type) {
case 'switch':
$tr.find('.value').append(`
<label class="SBSE-switch">
<input type="checkbox" data-config="${detail.configItem}">
<span class="SBSE-switch__slider"></span>
</label>
`);
break;
case 'text':
$tr.find('.value').append(`<input type="text" data-config="${detail.configItem}" value="${config.get(detail.configItem)}">`);
break;
case 'button':
$tr.find('.value').append(`<button data-config="${detail.configItem}">${detail.textContent}</button>`);
break;
case 'select':
$tr.find('.value').append(`<select data-config="${detail.configItem}"></select>`);
if (detail.options) $tr.find('select').append(`${detail.options.map(x => `<option value="${x}">${x}</option>`)}`);
break;
default:
}
});
// append report section
$model.find('table').append(`
<tr>
<td class="name">${i18n.get('settingsReportIssues')}</td>
<td class="value">
<a href="https://keylol.com/t305330-1-1" target="_blank">其乐 Keylol</a>
<a href="https://github.com/clancy-chao/Steam-Bundle-Sites-Extension/issues" target="_blank">GitHub</a>
</td>
</tr>
`);
// apply settings
const $sessionID = $model.find('[data-config="sessionID"]');
const $language = $model.find('[data-config="language"]');
const $ASFIPC = $model.find('[data-config^="ASFIPC"], [data-config^="ASFWS"]');
// toggles
$model.find('.SBSE-switch input[type="checkbox"]').each((i, input) => {
const $input = $(input);
$input.prop('checked', config.get(input.dataset.config));
$input.on('change', (e) => {
swal.showLoading();
const configItem = e.delegateTarget.dataset.config;
const state = e.delegateTarget.checked;
config.set(configItem, state);
if (configItem === 'autoUpdateSessionID') $sessionID.prop('disabled', state);
if (configItem === 'enableASFIPC') $ASFIPC.prop('disabled', !state);
setTimeout(swal.hideLoading, 500);
});
});
// toggle - disable related fields
// sessionID
$sessionID.prop('disabled', config.get('autoUpdateSessionID'));
$ASFIPC.prop('disabled', !config.get('enableASFIPC'));
// input text
$model.find('input[type="text"]').on('input', (e) => {
swal.showLoading();
const configItem = e.delegateTarget.dataset.config;
const value = e.delegateTarget.value.trim();
config.set(configItem, value);
setTimeout(swal.hideLoading, 500);
});
// select
$model.find('select').on('change', (e) => {
swal.showLoading();
const configItem = e.delegateTarget.dataset.config;
const value = e.delegateTarget.value;
config.set(configItem, value);
if (configItem === 'language') i18n.set();
setTimeout(swal.hideLoading, 500);
});
// select - language options
Object.keys(i18n.data).forEach((lang) => {
$language.append(new Option(i18n.data[lang].name, lang));
});
// select - language
$language.val(config.get('language'));
// select - ASF protocols
$ASFIPC.filter('select[data-config="ASFIPCProtocol"]').val(config.get('ASFIPCProtocol'));
$ASFIPC.filter('select[data-config="ASFWSProtocol"]').val(config.get('ASFWSProtocol'));
// button - sync library
$model.find('[data-config="syncLibrary"]').on('click', () => {
steam.sync([{
key: 'library',
notify: true
}]);
});
// button - select regions
$model.find('[data-config="highlightedRegions"]').on('click', () => {
swal({
title: i18n.get('settingshighlightedRegionsButton'),
width: '1200px',
onBeforeOpen: (dom) => {
const data = Object.assign({}, ISO2.name.english);;
const sortedCode = Object.keys(data).sort((a, b) => data[a] < data[b] ? -1 : data[a] > data[b] ? 1 : 0);
const separators = {
A: 'A',
B: 'B',
C: 'C',
D: 'D, E, F',
G: 'G, H, I',
J: 'J, K, L',
M: 'M',
N: 'N',
O: 'O, P, Q, R',
S: 'S',
T: 'T',
U: 'U, V, W, X, Y, Z',
};
let html = '';
sortedCode.forEach((code) => {
if (separators[data[code].charAt(0)]) {
html += `<span class="separator">${separators[data[code].charAt(0)]}</span>`;
separators[data[code].charAt(0)] = undefined;
}
html += `<span data-code="${code}">${ISO2.get(code)}</span>`;
});
$(dom).find('.swal2-content').append(`<div class="SBSE-grid">${html}</div>`);
$(dom).find('.swal2-content span[data-code]').on('click', (e) => {
$(e.delegateTarget).toggleClass('selected');
});
config.get('highlightedRegions').forEach((code) => {
$(dom).find(`.swal2-content span[data-code="${code}"]`).addClass('selected');
});
},
onClose: (dom) => {
config.set('highlightedRegions', $(dom).find(`.swal2-content span[data-code].selected`).map((i, ele) => $(ele).attr('data-code')).get());
},
onAfterClose: settings.display,
});
});
this.model = $model;
},
};
const SBSE = {
model: null,
handlers: {
extract() {
return {
items: []
};
},
retrieve() {
const $model = SBSE.getModel();
const data = this.extract();
const keys = [];
const includeTitle = $model.find('.SBSE-checkbox-title').prop('checked');
const joinKeys = $model.find('.SBSE-checkbox-join').prop('checked');
const selected = $model.find('.SBSE-select-filter').val() || 'All';
const skipUsed = $model.find('.SBSE-checkbox-skipUsed').prop('checked');
const skipMarketListing = !$model.find('.SBSE-checkbox-marketListings').prop('checked');
const separator = joinKeys ? ',' : eol;
const prefix = joinKeys && config.get('ASFFormat') ? '!redeem ' : '';
for (let i = 0; i < data.items.length; i += 1) {
const item = data.items[i];
let skip = false;
if (selected === 'Owned' && !item.owned) skip = true;
if (selected === 'NotOwned' && item.owned) skip = true;
if (skipUsed && item.used) skip = true;
if (skipMarketListing && item.marketListing) skip = true;
if (!skip) {
const temp = [item.key];
if (config.get('ASFFormat')) {
if (!joinKeys) temp.unshift(item.title);
keys.push(temp.join(tab));
} else {
if (includeTitle) temp.unshift(item.title);
if (config.get('titleComesLast')) temp.reverse();
keys.push(temp.join(', '));
}
}
}
$model.find('textarea').val(prefix + keys.join(separator));
},
activate(e) {
const $textarea = SBSE.getModel().find('textarea');
const keys = unique($textarea.val().match(regKey));
if (keys.length > 0) {
const $activateBtn = $(e.currentTarget);
$activateBtn.prop('disabled', true).addClass('SBSE-button--working');
$textarea.prop('disabled', true);
$textarea.val(keys.join(eol));
activator.activate(keys, () => {
$activateBtn.prop('disabled', false).removeClass('SBSE-button--working');
$textarea.prop('disabled', false);
});
} else $textarea.val(i18n.get('emptyInput'));
},
copy() {
SBSE.getModel().find('textarea').select();
document.execCommand('copy');
},
reset() {
SBSE.getModel().find('textarea').val('');
},
export (e) {
const data = this.extract();
if (data.items.length > 0) {
const exportBtn = e.target;
exportBtn.removeAttribute('href');
exportBtn.removeAttribute('download');
const fileType = exportBtn.dataset.filetype || 'txt';
const filename = data.filename.replace(/[\\/:*?"<>|!]/g, '');
const separator = {
txt: ', ',
csv: ',',
keys: tab,
};
const formattedData = data.items.map((line) => {
const temp = [];
if (line.title) temp.push(line.title.replace(/,/g, ' '));
temp.push(line.key);
return temp.join(separator[fileType]);
}).join(eol);
exportBtn.href = `data:text/${fileType};charset=utf-8,\ufeff${encodeURIComponent(formattedData)}`;
exportBtn.download = `${filename}.${fileType}`;
}
},
},
setHandlers(handlers) {
this.handlers = Object.assign(this.handlers, handlers);
},
getModel() {
return this.model instanceof $ ? this.model : $();
},
init() {
// construct SBSE model
const $model = $('<div class="SBSE-container__content__model" data-feature="SBSE"></div>');
$model.append(`
<textarea></textarea>
<div>
<button class="SBSE-button SBSE-button-reveal">${i18n.get('buttonReveal')}</button>
<button class="SBSE-button SBSE-button-retrieve">${i18n.get('buttonRetrieve')}</button>
<button class="SBSE-button SBSE-button-activate">${i18n.get('buttonActivate')}</button>
<button class="SBSE-button SBSE-button-copy">${i18n.get('buttonCopy')}</button>
<button class="SBSE-button SBSE-button-reset">${i18n.get('buttonReset')}</button>
<div class="SBSE-dropdown SBSE-dropdown-export">
<button class="SBSE-button SBSE-button-export">${i18n.get('buttonExport')}</button>
<ul class="SBSE-dropdown__list SBSE-dropdown__list-export">
<li><a data-fileType="txt">.txt</a></li>
<li><a data-fileType="csv">.csv</a></li>
<li><a data-fileType="keys">.keys</a></li>
</ul>
</div>
<label><input type="checkbox" class="SBSE-checkbox SBSE-checkbox-title" data-config="SBSE_ChkTitle">${i18n.get('checkboxIncludeGameTitle')}</label>
<label><input type="checkbox" class="SBSE-checkbox SBSE-checkbox-join" data-config="SBSE_ChkJoin">${i18n.get('checkboxJoinKeys')}</label>
<select class="SBSE-select SBSE-select-filter">
<option value="All" selected>${i18n.get('selectFilterAll')}</option>
<option value="Owned">${i18n.get('selectFilterOwned')}</option>
<option value="NotOwned">${i18n.get('selectFilterNotOwned')}</option>
</select>
<button class="SBSE-button-setting"> </button>
</div>
`);
// bind handlers
const handlers = this.handlers;
$model.find('button').click((e) => {
e.preventDefault();
});
$model.find('.SBSE-button-reveal').on('click.reveal', (e) => {
handlers.reveal(e);
});
$model.find('.SBSE-button-retrieve').on('click.retrieve', (e) => {
handlers.retrieve(e);
});
$model.find('.SBSE-button-activate').on('click.activate', (e) => {
handlers.activate(e);
});
$model.find('.SBSE-button-copy').on('click.copy', (e) => {
handlers.copy(e);
});
$model.find('.SBSE-button-reset').on('click.reset', (e) => {
handlers.reset(e);
});
$model.find('.SBSE-dropdown__list-export').on('click.export', (e) => {
handlers.export(e);
});
$model.find('.SBSE-button-setting').on('click.setting', settings.display);
$model.find('.SBSE-checkbox').on('change', (e) => {
const key = e.currentTarget.dataset.config;
if (key.length > 0) config.set(key, e.currentTarget.checked);
});
// apply settings
if (config.get('SBSE_ChkTitle')) $model.find('.SBSE-checkbox-title').prop('checked', true);
if (config.get('SBSE_ChkJoin')) $model.find('.SBSE-checkbox-join').prop('checked', true);
this.model = $model;
},
};
const ASF = {
model: null,
terminal: {},
getModel() {
return this.model instanceof $ ? this.model : $();
},
scrollToBottom(key) {
const terminal = this.terminal[key];
if (terminal instanceof $) terminal.scrollTop(terminal[0].scrollHeight);
},
push(key, line) {
this.terminal[key].append(`<span class="SBSE-terminal__message">${line}</span>`);
this.scrollToBottom(key);
},
listenLogs() {
const self = this;
self.push('log', 'Establishing connection to ASF IPC server');
const protocol = config.get('ASFWSProtocol');
const domain = `${config.get('ASFIPCServer')}:${config.get('ASFIPCPort')}`;
const password = config.get('ASFIPCPassword');
const url = `${protocol}://${domain}/Api/NLog${password.length > 0 ? `?password=${password}` : ''}`;
try {
const ws = new WebSocket(url);
ws.addEventListener('open', () => {
self.push('log', 'Connection established');
});
ws.addEventListener('error', () => {
self.push('log', 'An error occured while connecting to ASF IPC server');
});
ws.addEventListener('message', (e) => {
try {
const data = JSON.parse(e.data);
self.push('log', data.Result);
} catch (error) {
self.push('log', error.stack);
}
});
} catch (error) {
self.push('log', `Failed to establish connection, error message: ${error.message}`);
}
},
initCommands: async () => {
const ipc = {
protocol: config.get('ASFIPCProtocol'),
server: config.get('ASFIPCServer'),
port: config.get('ASFIPCPort'),
password: config.get('ASFIPCPassword'),
commands: {},
bots: [],
};
const self = ASF;
const requestOptions = (method, pathname) => {
const options = {
method
};
options.url = `${ipc.protocol}://${ipc.server}:${ipc.port + pathname}`;
if (ipc.password.length > 0) options.headers = {
Authentication: ipc.password
};
return options;
};
const sendCommand = async (command) => {
self.push('commands', command);
const res = await request(requestOptions('POST', `/Api/Command/${encodeURIComponent(command)}`));
try {
const data = JSON.parse(res.response);
const msg = data.Success === true ? data.Result : data.Message;
self.push('commands', msg);
} catch (error) {
self.push('commands', error.stack);
}
};
// append terminal input
const $input = $(`
<span class="SBSE-terminal__input">
<input type="text">
<input type="text">
</span>
`).appendTo(self.terminal.commands).find('input:first-child');
const $hint = $input.next('input');
// bind event
// display hint on input
$input.on('input', () => {
let newHint = '';
let saved = $hint.attr('data-saved');
const typed = $input.val().replace(/\s+/g, ' ');
if (typed.length > 0) {
const typedPieces = typed.split(' ');
// perform a new search for command
if (!saved || saved.length === 0 || saved.indexOf(typedPieces[0]) === -1) {
saved = Object.keys(ipc.commands).find(x => x.indexOf(typedPieces[0]) === 0) || '';
}
const command = ipc.commands[saved];
// found matching command
if (isArray(command) && command.length > 0) {
const hintPieces = command.slice(0);
// skip 1st piece as no need to process the command
for (let i = 1; i < typedPieces.length; i += 1) {
if (typedPieces[i].length > 0) {
let newHintPiece = command[i];
// replace command argument if typed something
if (typedPieces[i].length > 0) newHintPiece = typedPieces[i];
// match bot name
if (command[i] === '<Bots>' || command[i] === '<TargetBot>') {
const found = ipc.bots.find(x => x.indexOf(typedPieces[i]) === 0);
if (found) newHintPiece = found;
}
// multiple arguments for last typed piece
if (i === typedPieces.length - 1 &&
newHintPiece.includes(',') &&
(command[i] === '<Bots>' ||
command[i] === '<GameIDs>' ||
command[i] === '<SteamIDs64>' ||
command[i] === '<AppIDs>' ||
command[i] === '<RealAppIDs>' ||
command[i] === '<AppIDsOrGameNames>' ||
command[i] === '<AppIDs,GameName>' ||
command[i] === '<Keys>' ||
command[i] === '<Modes>')) {
if (newHintPiece.slice(-1) === ',') {
newHintPiece += command[i];
} else if (command[i] === '<Bots>') {
const pieces = newHintPiece.split(',');
const last = pieces.length - 1;
const found = ipc.bots.find(x => x.indexOf(pieces[last]) === 0);
if (found) {
pieces[last] = found;
newHintPiece = pieces.join(',');
}
}
}
hintPieces[i] = newHintPiece;
}
}
newHint = hintPieces.filter(x => x.length > 0).join(' ');
}
} else saved = '';
$hint.attr('data-saved', saved);
$hint.val(newHint);
$input.val(typed);
});
// detect key board event
$input.on('keydown', (e) => {
// right arrow key, auto complete hint
if (e.keyCode === 39 && $hint.val().length > $input.val().length) {
const bracket = $hint.val().indexOf('<');
const text = bracket > -1 ? $hint.val().slice(0, bracket) : $hint.val();
$input.val(text);
}
// enter key, send command
if (e.keyCode === 13) {
sendCommand($input.val());
$input.val('');
$hint.val('');
}
});
// prevent the hint input getting focus
$hint.on('focus', () => {
$input.focus();
});
// focus input field when click empty space
self.terminal.commands.parent().on('click', (e) => {
if ($(e.target).is('.SBSE-terminal-commands')) $input.focus();
});
self.push('commands', 'Fetching commands from ASF wiki');
// fetch commands
const resCommands = await request({
method: 'GET',
url: 'https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Commands',
});
if (resCommands.status === 200) {
const html = resCommands.response.slice(resCommands.response.indexOf('<div id="wiki-body"'), resCommands.response.indexOf('<div id="wiki-rightbar"'));
const $html = $(html);
const commands = $html.find('h2:has(#user-content-commands-1) + table tbody tr td:first-child code').get().map(ele => ele.innerText.trim());
commands.forEach((command) => {
const pieces = command.split(' ');
ipc.commands[pieces[0]] = pieces;
});
self.push('commands', 'Commands successfully fetched');
} else self.push('commands', 'Failed to fetch commands from ASF wiki, please refrsh to try again');
// fetch bots
const resBots = await request(requestOptions('GET', '/api/bot/ASF'));
if (resBots.status === 200) {
try {
const data = JSON.parse(resBots.response);
ipc.bots = Object.keys(data.Result);
} catch (e) {
throw e;
}
}
},
init() {
// construct SBSE model
const $model = $('<div class="SBSE-container__content__model" data-feature="ASF"></div>');
$model.append(`
<div class="SBSE-terminal SBSE-terminal-commands"><div></div></div>
<div class="SBSE-terminal SBSE-terminal-log SBSE-terminal--show"><div></div></div>
<div>
<button class="SBSE-button SBSE-button-commands">${i18n.get('buttonCommands')}</button>
<button class="SBSE-button SBSE-button-log">${i18n.get('buttonLog')}</button>
<button class="SBSE-button-setting"> </button>
</div>
`);
$model.find('.SBSE-button-commands').on('click.commands', () => {
$model.find('.SBSE-terminal--show').removeClass('SBSE-terminal--show');
$model.find('.SBSE-terminal-commands').addClass('SBSE-terminal--show');
$model.find('.SBSE-terminal__input input:first-child').focus();
});
$model.find('.SBSE-button-log').on('click.log', () => {
$model.find('.SBSE-terminal--show').removeClass('SBSE-terminal--show');
$model.find('.SBSE-terminal-log').addClass('SBSE-terminal--show');
});
$model.find('.SBSE-button-setting').on('click.setting', settings.display);
this.model = $model;
this.terminal.log = $model.find('.SBSE-terminal-log > div');
this.terminal.commands = $model.find('.SBSE-terminal-commands > div');
if (config.get('enableASFIPC')) {
this.listenLogs();
this.initCommands();
} else {
this.push('commands', 'ASF IPC feature not enabled, please go to settings and enable this feature');
this.push('log', 'ASF IPC feature not enabled, please go to settings and enable this feature');
}
},
};
const container = {
self: null,
models: {},
get(feature, handlers) {
this.show(feature);
if (isObject(handlers)) {
if (feature === 'SBSE') SBSE.setHandlers(handlers);
if (feature === 'ASF') ASF.setHandlers(handlers);
}
return this.self;
},
show(feature) {
// nav
this.self.find('.SBSE-container__nav__item--show').removeClass('SBSE-container__nav__item--show');
this.self.find(`.SBSE-container__nav__item[data-feature="${feature}"]`).addClass('SBSE-container__nav__item--show');
// content
this.self.find('.SBSE-container__content__model--show').removeClass('SBSE-container__content__model--show');
this.self.find(`.SBSE-container__content__model[data-feature="${feature}"]`).addClass('SBSE-container__content__model--show');
},
init() {
this.self = $('<div class="SBSE-container"></div>');
const $nav = $('<div class="SBSE-container__nav"></div>').appendTo(this.self);
const $content = $('<div class="SBSE-container__content"></div>').appendTo(this.self);
// construct nav
$nav.append(`
<ul>
<li class="SBSE-container__nav__item" data-feature="SBSE"><span>Steam Ext</span></li>
<li class="SBSE-container__nav__item" data-feature="ASF"><span>ASF IPC</span></li>
</ul>
`);
// bind event
$nav.find('.SBSE-container__nav__item').on('click', (e) => {
const $target = $(e.delegateTarget);
if (!$target.hasClass('SBSE-container__nav__item--show')) {
container.show($target.attr('data-feature'));
}
});
// append models to content block
this.models.SBSE = SBSE.getModel();
this.models.ASF = ASF.getModel();
$content.append(Object.values(this.models));
},
};
const keylolTooltip = {
timeoutID: 0,
load(data) {
if (config.get('enableTooltips')) {
const $container = $('<div/>');
(Array.isArray(data) ? data : [data]).forEach((d) => {
let type = null;
if (has.call(d, 'sub')) type = 'sub';
if (has.call(d, 'app')) type = 'app';
if (type !== null) {
const url = `https://steamdb.keylol.com/tooltip?v=4#${type}/${d[type]}#steam_info_${type}_${d[type]}_1`;
$container.append(
$(`<iframe id="SBSE-tooltip_${type + d[type]}" class="SBSE-tooltip" data-url="${url}"></iframe>`)
.mouseenter(() => {
clearTimeout(this.timeoutID);
})
.mouseout(this.hide),
);
}
});
$('body').append($container);
}
},
show(e) {
const $target = $(e.currentTarget);
const json = $target.closest('.SBSE-item--processed').attr('data-gameinfo');
if (json.length > 0 && config.get('enableTooltips')) {
const data = JSON.parse(json);
const opened = !!$('.SBSE-tooltip--show').length;
['app', 'sub'].forEach((type) => {
const $tooltip = $(`#SBSE-tooltip_${type + data[type]}`);
if ($tooltip.length > 0 && !opened) {
// load tooltip
if (!$tooltip.attr('src')) $tooltip.attr('src', $tooltip.attr('data-url'));
$tooltip.css({
top: e.clientY,
left: e.clientX + 10,
}).addClass('SBSE-tooltip--show');
this.reposition($tooltip, $tooltip.height());
$tooltip[0].contentWindow.postMessage('show', '*'); // get height
$target.one('mouseout', () => {
this.timeoutID = setTimeout(this.hide.bind(keylolTooltip), 500);
});
}
});
}
},
hide() {
const $tooltip = $('.SBSE-tooltip--show');
if ($tooltip.length > 0) {
$tooltip.removeClass('SBSE-tooltip--show');
$tooltip[0].contentWindow.postMessage('hide', '*');
}
},
reposition($tooltip, height) {
const $window = $(window);
const $document = $(document);
const offsetTop = $tooltip.offset().top - $document.scrollTop();
const offsetLeft = $tooltip.offset().left - $document.scrollLeft();
const overflowX = (offsetLeft + $tooltip.width()) - ($window.width() - 20);
const overflowY = (offsetTop + height) - ($window.height() - 20);
if (overflowY > 0) $tooltip.css('top', offsetTop - overflowY);
if (overflowX > 0) $tooltip.css('left', offsetLeft - overflowX);
},
listen() {
window.addEventListener('message', (e) => {
if (e.origin === 'https://steamdb.keylol.com' && e.data.height && e.data.src) {
const $tooltip = $(`.SBSE-tooltip[src="${e.data.src}"]`);
$tooltip.height(e.data.height);
this.reposition($tooltip, e.data.height);
}
});
},
};
const siteHandlers = {
indiegala() {
// inject css
GM_addStyle(`
.SBSE-container { margin-top: 10px; }
.SBSE-container__nav__item--show { border-bottom: 1px solid #CC001D; color: #CC001D; }
.SBSE-container__content__model > textarea { border: 1px solid #CC001D; border-radius: 3px; }
.SBSE-button { width: 100px; background-color: #CC001D; color: white; border: none; border-radius: 3px; }
.swal2-popup .SBSE-switch__slider { margin: 0; }
.SBSE-icon { margin-top: 15px; }
`);
const handlers = {
extract() {
const $tabCont = $('.profile-private-page-library-tab-cont');
const $source = $tabCont.length > 1 ? $tabCont.filter('.profile-private-page-library-tab-active') : $tabCont;
const bundleTitle = $('.profile-private-page-library-selected .profile-private-page-library-title').text().trim();
const data = {
title: bundleTitle,
filename: `IndieGala ${bundleTitle} Keys`,
items: [],
};
$source.find('ul[class^="profile-private-page"][class$="-active"]').find('.profile-private-page-library-subitem').each((i, ele) => {
const $ele = $(ele);
const key = $ele.find('input[class*="key-serial"]').val();
if (key) {
const d = JSON.parse($(ele).attr('data-gameinfo') || '{}');
if (Object.keys(d).length === 0) {
const $a = $ele.find('a[href*="steam"]');
const matched = $a.attr('href').match(/steam.+\/(app|sub)\/(\d+)/);
d.title = $ele.find('.profile-private-page-library-title *[title]').attr('title').trim();
if (matched) d[matched[1]] = parseInt(matched[2], 10);
}
d.key = key;
activator.pushKeyDetails(d);
data.items.push(d);
}
});
return data;
},
reveal(e) {
const $tabCont = $('.profile-private-page-library-tab-cont');
const $source = $tabCont.length > 1 ? $tabCont.filter('.profile-private-page-library-tab-active') : $tabCont;
const $revealBtn = $(e.currentTarget);
const selected = $('.SBSE-select-filter').val() || 'All';
const handler = ($games, callback) => {
const game = $games.shift();
if (game) {
const d = JSON.parse($(game).closest('.SBSE-item--processed').attr('data-gameinfo') || '{}');
if (selected === 'All' || (selected === 'Owned' && d.owned) || (selected === 'NotOwned' && !d.owned)) {
game.click();
unsafeWindow.getSerialKeyGo = true; // fix: issue#27
setTimeout(handler.bind(null, $games, callback), 700);
} else setTimeout(handler.bind(null, $games, callback), 1);
} else callback();
};
$revealBtn.addClass('SBSE-button--working');
handler($source.find('button[onclick^="getSerialKey"]'), () => {
$revealBtn.removeClass('SBSE-button--working');
$('.SBSE-button-retrieve').click();
});
},
};
const process = ($nodes) => {
const tooltipsData = [];
const $source = $nodes && $nodes.length > 0 ? $nodes : $('.profile-private-page-library-subitem');
$source.each((i, ele) => {
const $ele = $(ele);
const $a = $ele.find('a[href*="steam"]');
const d = {
title: $ele.find('.profile-private-page-library-title *[title]').attr('title').trim(),
};
if ($a.length > 0) {
const matched = $a.attr('href').match(/steam.+\/(app|sub)\/(\d+)/);
if (matched) d[matched[1]] = parseInt(matched[2], 10);
// check if owned & wished
d.owned = steam.isOwned(d);
d.wished = steam.isWished(d);
if (d.owned) $ele.addClass('SBSE-item--owned');
if (d.wished) $ele.addClass('SBSE-item--wished');
}
// append icon
$ele.find('.profile-private-page-library-title').after(
$('<span class="SBSE-icon"></span>').mouseenter(keylolTooltip.show.bind(keylolTooltip)),
);
tooltipsData.push(d);
$ele.attr('data-gameinfo', JSON.stringify(d)).addClass('SBSE-item--processed SBSE-item--steam');
});
// load Keylol tooltip
keylolTooltip.load(tooltipsData);
};
const $container = container.get('SBSE', handlers);
process();
// insert container
$('.profile-private-page-library-menu').eq(0).before($container);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).forEach((addedNode) => {
if (addedNode.nodeType === 1 && addedNode.classList.contains('profile-private-page-library-subitem')) process($(addedNode));
});
});
});
observer.observe($('.profile-private-page-library-cont')[0], {
childList: true,
subtree: true,
});
},
fanatical() {
// inject css
GM_addStyle(`
.SBSE-container { margin-top: 10px; }
.SBSE-container__nav { background-color: rgb(28, 28, 28); }
.SBSE-container__nav__item--show {
border-bottom: 1px solid #ff9800;
color: #ff9800;
}
.SBSE-container__content { margin: 0; }
.SBSE-container__content__model > textarea { background-color: #434343; color: #eee; }
.SBSE-container__content__model label { color: #DEDEDE; }
.SBSE-button, .SBSE-select { border: 1px solid transparent; background-color: #1c1c1c; color: #eee; }
.SBSE-button:hover, .SBSE-select:hover { color: #A8A8A8; }
.SBSE-button--narrow { width: 80px; }
/* currency converter */
.SBSE-priceExt { positon: relative; }
.SBSE-priceExt ~ .SBSE-priceExt { display: none; }
.SBSE-priceExt--portrait { width: 100%; padding: 0 .875rem 0 .875rem; }
.SBSE-priceExt--portrait > div { padding: 1rem; }
.SBSE-priceExt--portrait .SBSE-priceExt__currencyToggler {
width: 100%; height: 40px;
margin-bottom: 10px;
font-size: 20px;
border-radius: 3px;
}
.SBSE-priceExt--landscape { padding: 1rem; }
.SBSE-priceExt--landscape > div { display: flex; align-items: center; justify-content: space-evenly; }
.SBSE-priceExt--landscape .SBSE-priceExt__currencyToggler {
width: 300px; height: 40px;
font-size: 20px;
border-radius: 3px;
}
.SBSE-priceExt__pricingDetail { background-color: transparent; }
.SBSE-priceExt__pricingDetail th { padding-top: 10px; }
.SBSE-priceExt__pricingDetail .cheapest { border-bottom: 1px solid #ff9800; font-weight: bold; }
.SBSE-priceExt__pricingDetail .currency-flag { vertical-align: text-bottom; }
.swal2-popup table { background-color: white; }
.SBSE-icon { vertical-align: bottom; }
`);
const fetchAPIData = async (s, c) => {
let slug = s;
let callback = c;
if (typeof s === 'function') {
callback = s;
slug = location.href.split('/').pop();
}
let JSONString = GM_getValue(`Fanatical-${slug}`, '');
if (JSONString.length === 0) {
const res = await fetch(`https://www.fanatical.com/api/products/${slug}`);
if (res.ok) {
JSONString = await res.text();
GM_setValue(`Fanatical-${slug}`, JSONString);
} else JSONString = '{}';
}
if (typeof callback === 'function') callback(JSON.parse(JSONString));
};
const productHandler = async (APIData) => {
if (Object.keys(APIData).length > 0) {
const language = config.get('language');
const $priceExt = $(`
<div class="SBSE-priceExt SBSE-priceExt--portrait">
<div>
<select class="SBSE-priceExt__currencyToggler"></select>
</div>
</div>
`);
const $currencyToggler = $priceExt.find('.SBSE-priceExt__currencyToggler');
const $pricingDetail = $('<table class="SBSE-priceExt__pricingDetail"></table>');
const selectedCurrency = GM_getValue('SBSE_selectedCurrency', 'USD');
const isStarDeal = !!$('.stardeal-purchase-info').length;
let starDeal = {};
if (isStarDeal) {
// fetch star-deal data
const res = await fetch('https://www.fanatical.com/api/star-deal');
if (res.ok) starDeal = await res.json();
}
// change orientation
if (isStarDeal || $('.background-bundle, .bundle-header.container-fluid').length > 0) {
$priceExt.toggleClass('SBSE-priceExt--portrait SBSE-priceExt--landscape container');
}
Object.keys(xe.currencies).forEach((currency) => {
const selected = currency === selectedCurrency ? ' selected' : '';
$currencyToggler.append(
$(`<option value="${currency}"${selected}>${xe.currencies[currency][language]}</option>`),
);
});
$currencyToggler.change(() => {
xe.update($currencyToggler.val());
});
// bundle page
APIData.bundles.forEach((tier, index) => {
const $detail = $pricingDetail.clone();
if (APIData.bundles.length > 1) $detail.append(`<tr><th colspan="3">Tier ${index + 1}</th></tr>`);
Object.keys(tier.price).sort().forEach((currency) => {
const value = tier.price[currency];
const symbol = xe.currencies[currency].symbol;
const decimalPlace = xe.currencies[currency].decimal ? 2 : 0;
$detail.append(`
<tr class="tier${index + 1}">
<td><div class="currency-flag currency-flag-${currency.toLowerCase()}"></div></td>
<td>${symbol + (value / 100).toFixed(decimalPlace)}</td>
<td> ≈ <span class="SBSE-price" data-currency="${currency}" data-value="${value}"></span></td>
</tr>
`);
});
$detail.appendTo($currencyToggler.parent());
});
// game page
if (location.href.includes('/game/') || location.href.includes('/dlc/')) {
let discount = 1;
if (has.call(APIData, 'current_discount') &&
new Date(APIData.current_discount.until).getTime() > Date.now()
) discount = 1 - APIData.current_discount.percent;
if (isStarDeal) discount = 1 - ($('.discount-percent').text().replace(/\D/g, '') / 100);
Object.keys(APIData.price).sort().forEach((currency) => {
let value = Math.trunc(APIData.price[currency] * discount);
const symbol = xe.currencies[currency].symbol;
const decimalPlace = xe.currencies[currency].decimal ? 2 : 0;
// if star-deal data loaded successfully
if (has.call(starDeal, 'promoPrice')) value = starDeal.promoPrice[currency];
$pricingDetail.append(`
<tr class="tier1">
<td><div class="currency-flag currency-flag-${currency.toLowerCase()}"></div></td>
<td>${symbol + (value / 100).toFixed(decimalPlace)}</td>
<td> ≈ <span class="SBSE-price" data-currency="${currency}" data-value="${value}"></span></td>
</tr>
`).appendTo($currencyToggler.parent());
});
}
$('.product-commerce-container').append($priceExt);
$('.stardeal-purchase-info, .bundle-header').filter(':visible').eq(0).after($priceExt);
xe.update(selectedCurrency);
// highlight the cheapest
for (let i = 1; i < 10; i += 1) {
const $prices = $(`.tier${i} .SBSE-price`);
if ($prices.length === 0) break;
$($prices.toArray().sort((a, b) => a.textContent.replace(/\D/g, '') - b.textContent.replace(/\D/g, '')).shift()).closest('tr').addClass('cheapest');
}
}
};
const handlers = {
extract() {
const bundleTitle = $('h5').eq(0).text().trim();
const data = {
title: bundleTitle,
filename: `Fanatical ${bundleTitle} Keys`,
items: [],
};
$('.account-content .order-item-details-container').each((i, orderItem) => {
const $orderItem = $(orderItem);
const key = $orderItem.find('input[type="text"]').val();
if (key) {
const d = JSON.parse($orderItem.closest('.SBSE-item--processed').attr('data-gameinfo') || '{}');
if (Object.keys(d).length === 0) {
d.title = $orderItem.find('.game-name').text().trim();
}
d.key = key;
activator.pushKeyDetails(d);
data.items.push(d);
}
});
return data;
},
reveal(e) {
const $revealBtn = $(e.currentTarget);
const selected = $('.SBSE-select-filter').val() || 'All';
const handler = ($games, callback) => {
const game = $games.shift();
if (game) {
const d = JSON.parse($(game).closest('.SBSE-item--processed').attr('data-gameinfo') || '{}');
if (selected === 'All' || (selected === 'Owned' && d.owned) || (selected === 'NotOwned' && !d.owned)) {
game.click();
setTimeout(handler.bind(null, $games, callback), 300);
} else setTimeout(handler.bind(null, $games, callback), 1);
} else setTimeout(callback, 500);
};
$revealBtn.addClass('SBSE-button--working');
handler($('.account-content .key-container button'), () => {
$revealBtn.removeClass('SBSE-button--working');
$('.SBSE-button-retrieve').click();
});
},
};
const process = ($node) => {
// empty textarea
SBSE.getModel().find('textarea').val('');
// retrieve title
$('.account-content h5').each((i, h5) => {
const title = h5.textContent.trim();
const slug = title.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '');
fetchAPIData(slug, (APIData) => {
if (Object.keys(APIData).length > 0) {
const tooltipsData = [];
const matchGame = (data) => {
if (has.call(data, 'steam') && data.steam.id) {
const $gameTitle = $node.find(`.order-item .game-name:contains(${data.name})`).filter((index, name) => data.name === name.textContent.trim());
const $orderItem = $gameTitle.closest('.order-item');
const d = {
title: data.name,
app: parseInt(data.steam.id, 10),
};
d.owned = steam.isOwned(d);
d.wished = steam.isWished(d);
// check if owned & wished
if (d.owned) $orderItem.addClass('SBSE-item--owned');
if (d.wished) $orderItem.addClass('SBSE-item--wished');
// append Steam store link
$gameTitle.append(
`<span> | </span><a class="SBSE-link-steam_store" href="https://store.steampowered.com/app/${d.app}/" target="_blank">${i18n.get('steamStore')}</a>`,
$('<span class="SBSE-icon"></span>').mouseenter(keylolTooltip.show.bind(keylolTooltip)),
);
tooltipsData.push(d);
$orderItem.addClass('SBSE-item--processed SBSE-item--steam').attr('data-gameinfo', JSON.stringify(d));
}
};
matchGame(APIData);
APIData.bundles.forEach((tier) => {
tier.games.forEach(matchGame);
});
// load Keylol tooltip
keylolTooltip.load(tooltipsData);
}
});
});
};
const $container = container.get('SBSE', handlers);
$container.find('.SBSE-button').addClass('SBSE-button--narrow'); // narrow buttons
$container.find('a').attr('href', ''); // dodge from master css selector
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).filter(x => x.nodeType === 1).forEach((node) => {
const $node = $(node);
const currentURL = location.href;
// url changed
if (node.matches('[property="og:url"]')) {
if (currentURL.includes('/bundle/') ||
currentURL.includes('/game/') ||
currentURL.includes('/dlc/')
) fetchAPIData(productHandler);
}
// order contents loaded
if ($node.is('.order-item') || $node.children('div.order-bundle-items-container, div.order-item').length > 0) {
if (currentURL.includes('/orders/')) {
// insert container
const $anchor = $('.account-content h3');
if ($('.SBSE_container').length === 0 && $anchor.length > 0) {
$anchor.parent().css({
'max-width': '100%',
'flex-basis': 'auto',
});
$anchor.eq(0).before($container);
}
}
if (currentURL.includes('/product-library')) {
// insert container
const $anchor = $('.key-list-container');
if ($('.SBSE_container').length === 0 && $anchor.length > 0) $anchor.eq(0).before($container);
}
process($node);
}
});
});
}).observe($('html')[0], {
childList: true,
subtree: true,
});
},
humblebundle() {
// inject css
GM_addStyle(`
.SBSE-container__content__model > div { position: relative; }
.SBSE-container__content__model > textarea {
border: 1px solid #CFCFCF;
border-radius: 5px;
color: #4a4c45;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
.SBSE-button {
width: 70px;
border: 1px solid #C9CCD3;
border-radius: 3px;
background-color: #C5C5C5;
background: linear-gradient(to top, #cacaca, #e7e7e7);
color: #4a4c45 !important;
}
.SBSE-button:hover {
border: 1px solid #b7bac0;
background-color: #fafcff;
color: #555961 !important;
}
.SBSE-button--narrow.SBSE-button--working { width: 76px; padding-right: 36px; }
.SBSE-button-setting { position: absolute; right: 0; }
.SBSE-item--owned .sr-unredeemed-steam-button {
background-color: #F3F3F3;
background: linear-gradient(to top, #E8E8E8, #F6F6F6);
}/*
.SBSE-item--owned .heading-text h4 > span:not(.steam-owned):last-child::after {
content: '\\f085';
font-family: hb-icons;
color: #17A1E5;
}*/
.SBSE-activationRestrictions-title {
margin: 0 0 5px;
display: flex;
positon: relative;
cursor: pointer;
}
.SBSE-activationRestrictions-title::before, .SBSE-activationRestrictions-title::after { padding: 0 5px; }
.SBSE-activationRestrictions-title::before { content: '+'; display: none; order: 2; }
.SBSE-activationRestrictions-title::after { content: '-'; display: block; order: 3; }
.SBSE-activationRestrictions-details p { margin: 0; }
.SBSE-activationRestrictions-details .highlight { color: crimson; }
.SBSE-activationRestrictions--collapsed > h5::before { display: block; }
.SBSE-activationRestrictions--collapsed > h5::after { display: none; }
.SBSE-activationRestrictions--collapsed > div { display: none; }
.swal2-icon-text { font-size: inherit; }
.flag-icon { width: 4em; height: 3em; border-radius: 3px; }
.flag-icon-unknown { border: 1px solid; text-align: center; line-height: 3em; }
.key-redeemer:not(:first-child) h4 { margin-top: 50px; }
.key-redeemer h4 { position: relative; margin-bottom: 10px; }
.key-redeemer .SBSE-icon { position: absolute; top: 50%; margin-top: -10px; }
`);
let gamekey;
const atDownload = location.pathname === '/downloads';
const fetchKey = async ($node, machineName, callback) => {
if (gamekey) {
const res = await fetch('https://www.humblebundle.com/humbler/redeemkey', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Origin: 'https://www.humblebundle.com',
Referer: location.href,
},
body: `keytype=${machineName}&key=${gamekey}&keyindex=0`,
credentials: 'same-origin',
});
if (res.ok) {
const d = await res.json();
if (d.success) {
$node.closest('.container').html(`
<div title="${d.key}" class="js-keyfield keyfield redeemed enabled">
<div class="keyfield-value">${d.key}</div>
<a class="steam-redeem-button js-steam-redeem-button" href="https://store.steampowered.com/account/registerkey?key=${d.key}" target="_blank">
<div class="steam-redeem-text">Redeem</div>
<span class="tooltiptext">Redeem on Steam</span>
</a>
<div class="spinner-icon" aria-label="Loading">
<i class="hb hb-spinner hb-spin"></i>
</div>
</div>
`);
} else swal(i18n.get('failTitle'), JSON.stringify(d), 'error');
} else $node.click();
if (typeof callback === 'function') callback();
} else $node.click();
};
const handlers = {
extract() {
const bundleTitle = $('title').text().split(' (').shift();
const data = {
title: bundleTitle,
filename: `Humble Bundle ${bundleTitle} Keys`,
items: [],
};
$('.keyfield.redeemed .keyfield-value').each((i, ele) => {
const $ele = $(ele);
const key = $ele.text().trim();
if (key) {
const d = JSON.parse($ele.closest('.SBSE-item--processed').attr('data-gameinfo') || '{}');
if (Object.keys(d).length === 0) {
const $titleEle = $ele.closest(atDownload ? '.container' : '.redeemer-cell').prev().find('h4');
d.title = $titleEle.contents().eq(0).text().trim();
}
d.key = key;
activator.pushKeyDetails(d);
data.items.push(d);
}
});
return data;
},
reveal(e) {
const $revealBtn = $(e.currentTarget);
const selected = $('.SBSE-select-filter').val() || 'All';
const handler = ($games, callback) => {
const game = $games.shift();
if (game) {
const $game = $(game);
const machineName = $game.closest('.key-redeemer').attr('data-machineName');
const d = JSON.parse($(game).closest('.SBSE-item--processed').attr('data-gameinfo') || '{}');
if (atDownload && machineName) {
if (selected === 'All' || (selected === 'Owned' && d.owned) || (selected === 'NotOwned' && !d.owned)) {
fetchKey($game, machineName, () => {
handler($games, callback);
});
} else setTimeout(handler.bind(null, $games, callback), 1);
} else {
game.click();
$('.sr-warning-modal-confirm-button').click();
setTimeout(handler.bind(null, $games, callback), 200);
}
} else callback();
};
$revealBtn.addClass('SBSE-button--working');
handler($('.key-redeemer.SBSE-item--steam .keyfield:not(.redeemed)'), () => {
$revealBtn.removeClass('SBSE-button--working');
$('.SBSE-button-retrieve').click();
});
},
};
const process = async ($node) => {
gamekey = new URLSearchParams(location.search).get('key');
let json = GM_getValue(gamekey, '');
if (json.length === 0) {
const res = await fetch(`https://www.humblebundle.com/api/v1/order/${gamekey}?all_tpkds=true`, {
method: 'GET',
credentials: 'same-origin',
});
if (res.ok) json = await res.text();
}
try {
const data = JSON.parse(json);
const tooltipsData = [];
data.tpkd_dict.all_tpks.forEach((game) => {
const $keyRedeemer = $node.find(`.key-redeemer:has(.heading-text[data-title="${game.human_name.replace(/"/g, '\\"')}"])`);
if ($keyRedeemer.length > 0) {
if (game.key_type === 'steam') {
$keyRedeemer.addClass('SBSE-item--steam');
const d = {
title: game.human_name,
app: parseInt(game.steam_app_id, 10),
sub: parseInt(game.steam_package_id, 10),
};
d.owned = steam.isOwned(d);
d.wished = steam.isWished(d);
// apply owned effect on game title
if (d.owned) $keyRedeemer.addClass('SBSE-item--owned');
if (d.wished) $keyRedeemer.addClass('SBSE-item--wished');
// store data
$keyRedeemer.attr({
'data-machineName': game.machine_name,
'data-humanName': game.human_name,
'data-gameinfo': JSON.stringify(d),
});
// append Steam store link
const $target = $keyRedeemer.find('h4 > span').eq(0);
if (d.app > 0) {
$target.after(`<span> | </span><a class="SBSE-link-steam_store" href="https://store.steampowered.com/app/${d.app}/" target="_blank">${i18n.get('steamStore')}</a>`);
}
if (d.sub > 0) {
$target.after(`<span> | </span><a class="SBSE-link-steam_db" href="https://steamdb.info/sub/${d.sub}/" target="_blank">Steam DB</a>`);
}
tooltipsData.push(d);
}
// activation restrictions
const $container = $('<div class="SBSE-activationRestrictions"></div>');
const $title = $(`<h5 class="SBSE-activationRestrictions-title">${i18n.get('HBActivationRestrictions')}</h5>`);
const $details = $('<div class="SBSE-activationRestrictions-details"></div>');
const disallowed = game.disallowed_countries.map(c => config.get('highlightedRegions').includes(c) ? `<span class="highlight">${ISO2.get(c)}</span>` : ISO2.get(c));
const exclusive = game.exclusive_countries.map(c => config.get('highlightedRegions').includes(c) ? `<span class="highlight">${ISO2.get(c)}</span>` : ISO2.get(c));
const comma = config.get('language').includes('chinese') ? '、' : ', ';
if (disallowed.length > 0) $details.append(`<p>${i18n.get('HBDisallowedCountries')}<br>${disallowed.join(comma)}</p>`);
if (exclusive.length > 0) $details.append(`<p>${i18n.get('HBExclusiveCountries')}<br>${exclusive.join(comma)}</p>`);
if (disallowed.length > 0 || exclusive.length > 0) {
$container.append($title, $details);
$keyRedeemer.find('.heading-text').after($container);
$title.on('click', () => {
$container.toggleClass('SBSE-activationRestrictions--collapsed');
});
}
$keyRedeemer.addClass('SBSE-item--processed');
}
});
// override default popups
document.addEventListener('click', (e) => {
const $target = $(e.target).closest('.keyfield:not(.redeemed, .redeemed-gift)');
const $keyRedeemer = $target.closest('.key-redeemer.SBSE-item--steam');
const machineName = $keyRedeemer.attr('data-machineName');
if ($target.length > 0 && $keyRedeemer.length > 0 && machineName) {
e.stopPropagation();
if ($keyRedeemer.hasClass('SBSE-item--owned')) {
swal({
title: i18n.get('HBAlreadyOwned'),
text: i18n.get('HBRedeemAlreadyOwned').replace('%title%', $keyRedeemer.attr('data-humanName')),
type: 'question',
showCancelButton: true,
}).then((result) => {
if (result.value) fetchKey($target, machineName);
});
} else fetchKey($target, machineName);
}
}, true);
// load Keylol tooltip
keylolTooltip.load(tooltipsData);
} catch (e) {
throw e;
}
};
const $container = container.get('SBSE', handlers);
const $keyManager = $('.js-key-manager-holder');
// narrow buttons
$container.find('.SBSE-button').addClass('SBSE-button--narrow');
// at home page
if ($keyManager.length > 0) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).forEach((addedNode) => {
if (addedNode.className === 'header') {
observer.disconnect();
$(addedNode).after($container);
}
});
});
});
observer.observe($keyManager[0], {
childList: true
});
// at download page
} else {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).forEach(async (addedNode) => {
const $node = $(addedNode);
if ($node.hasClass('key-list') || $node.find('.key-list').length > 0) {
observer.disconnect();
$node.closest('.whitebox-redux').before($container);
// fetch game heading & wrap heading
$node.find('.heading-text > h4').each((i, heading) => {
heading.parentElement.dataset.title = heading.innerText.trim();
$(heading.firstChild).wrap('<span/>');
$(heading).append(
$('<span class="SBSE-icon"></span>').mouseenter(keylolTooltip.show.bind(keylolTooltip)),
);
});
// fetch & process key data
process($node);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
// append user's region
const countryCode = unsafeWindow.models.request.country_code;
if (countryCode) {
const code = countryCode.toLowerCase();
const countryName = ISO2.get(countryCode);
const $flag = $(`<span class="flag-icon flag-icon-unknown" tooltip="${i18n.get('HBCurrentLocation')}?"></span>`);
if (GM_getResourceText('flagIcon').includes(`${code}.svg`)) {
$flag.toggleClass(`flag-icon-unknown flag-icon-${code}`).attr('tooltip', i18n.get('HBCurrentLocation') + countryName);
} else $flag.text('?');
$('.navbar-content').prepend($flag);
}
},
dailyindiegame() {
const MPHideList = JSON.parse(GM_getValue('SBSE_DIGMPHideList') || '[]');
const pathname = location.pathname;
if (pathname.includes('/account_page') || pathname.includes('/account_update')) {
// force sync library
steam.sync([{
key: 'library'
}]);
// update DIG balance
const balanceText = $('a[href*="transactionhistory.html"]').eq(0).closest('div').text().match(/\$\d+\.\d+/);
const balance = balanceText ? parseInt(balanceText[0].replace(/\D/g, ''), 10) : '';
if (!isNaN(balance)) GM_setValue('SBSE_DIGBalance', balance);
// inject css
GM_addStyle(`
.SBSE-container { padding: 5px; border: 1px solid #424242; }
.SBSE-container__nav__item--show {
border-bottom: 1px solid #FD5E0F;
color: #FD5E0F;
}
.SBSE-container__content__model > textarea { border: 1px solid #000; }
.SBSE-button {
border: none;
background-color: #FD5E0F;
color: rgb(49, 49, 49);
font-family: Ropa Sans;
font-size: 15px;
font-weight: 600;
}
`);
const handlers = {
extract() {
const data = {
title: 'DailyIndieGame Keys',
filename: 'DailyIndieGame Keys',
items: [],
};
$('#TableKeys tr').each((i, tr) => {
const $tds = $(tr).children();
const key = $tds.eq(4).text().trim();
if (key.includes('-')) {
const d = {
title: $tds.eq(2).text().trim(),
key,
marketListing: $tds.eq(6).text().includes('Cancel trade'),
};
activator.pushKeyDetails(d);
data.items.push(d);
}
});
return data;
},
reveal() {
const $form = $('#form3');
$('#quickaction').val(1);
$.ajax({
method: 'POST',
url: $form.attr('action'),
data: $form.serializeArray(),
success() {
location.reload();
},
});
},
};
const $container = container.get('SBSE', handlers);
$container.find('.SBSE-button-export, .SBSE-select-filter').remove();
$container.find('label:has(.SBSE-checkbox-join)').after(`
<label><input type="checkbox" class="SBSE-checkbox-marketListings">${i18n.get('checkboxMarketListings')}</label>
`); // append checkbox for market keys
$('#TableKeys').eq(0).before($container);
// rate all positive
const $awaitRatings = $('a[href^="account_page_0_ratepositive"]');
if ($awaitRatings.length > 0) {
$('#TableKeys td:contains(Rate TRADE)').text(i18n.get('DIGRateAllPositive')).css('cursor', 'pointer').click(() => {
$awaitRatings.each(async (i, a) => {
const res = await fetch(a.href, {
method: 'GET',
credentials: 'same-origin',
});
if (res.ok) $(a).parent('td').html('<span class="DIG3_14_Orange">Positive</span>');
});
});
}
// DIG Menu
} else if (pathname.includes('/account_digstore') ||
pathname.includes('/account_trades') ||
pathname.includes('/account_tradesXT') ||
pathname.includes('/store_update') ||
pathname.includes('/storeXT_update') ||
pathname.includes('/site_content_marketplace')) {
// inject css styles
GM_addStyle(`
body.hideOwned .SBSE-item--owned,
body.hideOwned .SBSE-item--owned + .DIGMenu-searchResults { display: none; }
.headerRow > td:first-child { padding-left: 0; }
.headerRow > td:last-child { padding-right: 0; }
.DIGMenu > * { margin-right: 10px; padding: 4px 8px !important; cursor: pointer; }
.DIG-row { height: 30px; }
.DIGMenu button { padding: 4px 8px; outline: none; cursor: pointer; }
.DIG-row--checked { background-color: #222; }
.DIGMenu-searchResults td { padding: 0 }
.DIGMenu-searchResults iframe {
width: 100%; height: 300px;
display: none;
background-color: white;
border: none;
}
.SBSE-item--owned .DIG3_14_Gray { color: #9ccc65; }
.SBSE-item--wished .DIG3_14_Gray { color: #29b6f6; }
.SBSE-item--ignored .DIG3_14_Gray { text-decoration: line-through; }
.DIG2content select { max-width: 200px; }
#DIGSelectAll { display: none; }
#DIGSelectAll + span { display: inline-block; }
#DIGSelectAll ~ span:last-child { display: none; }
#DIGSelectAll:checked + span { display: none; }
#DIGSelectAll:checked ~ span:last-child { display: inline-block; }
.showOwnedListings { color: #FD5E0F; }
.showOwnedListings > label { vertical-align: text-bottom; }
.showOwnedListings input:checked + .SBSE-switch__slider { background-color: #FD5E0F; }
.DIGBalanceDetails > span { margin-right: 20px; }
.DIG__edit_balance {
display: inline-block;
position: relative;
transform: rotate(45deg);
cursor: pointer;
}
.DIG__edit_balance > span {
display: inline-block;
}
.DIG__edit_balance .tip {
width: 0; height: 0;
position: absolute;
top: 13px;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
border-top: 3px solid #999;
}
.DIG__edit_balance .body {
width: 4px; height: 12px;
background-color: #999;
}
.DIG__edit_balance .rubber {
width: 4px; height: 2px;
position: absolute;
top: -3px;
background-color: #999;
top: -3px;
}
`);
swal.showLoading();
// append menu buttons
const $target = $('#form3').closest('tr').children().eq(0);
const $DIGMenu = $(`
<div class="DIGMenu">
<label class="DIGSelectAll DIG3_Orange_15_Form">
<input type="checkbox" id="DIGSelectAll">
<span>${i18n.get('DIGMenuSelectAll')}</span>
<span>${i18n.get('DIGMenuSelectCancel')}</span>
</label>
<span class="DIGButtonPurchase DIG3_Orange_15_Form">${i18n.get('DIGMenuPurchase')}</span>
<label class="showOwnedListings">
<label class="SBSE-switch SBSE-switch--small">
<input type="checkbox" id="showOwnedListings" checked>
<span class="SBSE-switch__slider"></span>
</label>
<span>${i18n.get('owned')}</span>
</label>
</div>
`);
if ($target.children().length > 0) {
const $tr = $('<tr/>');
$tr.append($target.clone());
$target.parent().before($tr);
}
$target.empty().append($DIGMenu);
$target.parent().addClass('headerRow');
// bind button event
$('.DIGButtonPurchase').click(() => {
let balance = GM_getValue('SBSE_DIGBalance');
const $games = $('.DIG-row--checked:visible');
swal({
title: i18n.get('DIGButtonPurchasing'),
html: '<p></p>',
onOpen: () => {
swal.showLoading();
},
});
(async function purchaseHandler() {
const game = $games.shift();
if (game) {
const $game = $(game);
const id = $game.attr('data-id');
const price = parseInt($game.attr('data-price'), 10);
const title = $game.attr('data-title');
if (title.length > 0) swal.getContent().querySelector('p').textContent = title;
if (id && price > 0) {
if (balance - price >= 0) {
let url = `${location.origin}/account_buy.html`;
const requestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `quantity=1&xgameid=${id}&xgameprice1=${price}&send=Purchase`,
mode: 'same-origin',
credentials: 'same-origin',
cache: 'no-store',
referrer: `${location.origin}/account_buy_${id}.html`,
};
if (pathname === '/account_trades.html' || pathname === '/account_tradesXT.html' || pathname === '/site_content_marketplace.html') {
url = `${location.origin}/account_buytrade_${id}.html`;
requestInit.body = `gameid=${id}&send=Purchase`;
requestInit.referrer = url;
}
const res = await fetch(url, requestInit);
if (res.ok) {
$game.click();
balance -= price;
$('.DIG__current_balance').attr('data-value', balance);
}
purchaseHandler();
} else {
swal({
title: i18n.get('failTitle'),
text: i18n.get('DIGInsufficientFund'),
type: 'error',
});
}
} else purchaseHandler();
} else {
GM_setValue('SBSE_DIGBalance', balance);
swal({
title: i18n.get('successTitle'),
text: i18n.get('DIGFinishedPurchasing'),
type: 'success',
});
}
}());
});
$('#DIGSelectAll').on('change', (e) => {
const checked = e.delegateTarget.checked;
let total = 0;
$('.DIG-row:visible').toggleClass('DIG-row--checked', checked);
if (checked) {
total = $('.DIG-row--checked:visible').map((i, row) => parseInt(row.dataset.price, 10)).get().reduce((a, b) => a + b);
}
$('.DIG_total_amount').attr('data-value', total);
});
$('#showOwnedListings').on('change', (e) => {
const showOwnedListings = e.delegateTarget.checked;
const $rows = $('.DIG-row--checked.SBSE-item--owned');
$('body').toggleClass('hideOwned', !showOwnedListings);
GM_setValue('DIGShowOwnedListings', showOwnedListings);
if (!showOwnedListings && $rows.length > 0) {
const total = $rows.map((i, row) => parseInt(row.dataset.price, 10)).get().reduce((a, b) => a + b);
$rows.removeClass('DIG-row--checked');
$('.DIG_total_amount').attr('data-value', (i, value) => parseInt(value, 10) - total);
}
});
// menu settings
$('#showOwnedListings').prop('checked', GM_getValue('DIGShowOwnedListings', true)).change();
// append sync time and event
const seconds = Math.round((Date.now() - steam.lastSync('library')) / 1000);
$target.closest('table').before(`
<span> ${i18n.get('lastSyncTime').replace('%seconds%', seconds)}</span>
`);
// append balance details
$target.closest('table').before(`
<div class="DIGBalanceDetails">
<span>${i18n.get('DIGCurrentBalance')}$<span class="DIG__current_balance" data-value="0">0.00</span></span>
<span class="DIG__edit_balance">
<span class="tip"></span>
<span class="body"></span>
<span class="rubber"></span>
</span>
<span>${i18n.get('DIGTotalAmount')}$<span class="DIG_total_amount" data-value="0">0.00</span></span>
</div>
`);
// bind balance details event
$('.DIGBalanceDetails span[data-value]').each((i, span) => {
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-value') {
const target = mutation.target;
target.textContent = (target.dataset.value / 100).toFixed(2);
}
});
}).observe(span, {
attributes: true
});
});
$('.DIG__edit_balance').on('click', () => {
swal({
title: i18n.get('DIGEditBalance'),
input: 'number',
inputPlaceholder: i18n.get('DIGPoint'),
inputAttributes: {
min: 1
},
showCancelButton: true,
}).then((result) => {
if (!isNaN(result.value)) {
const balance = Math.trunc(result.value);
GM_setValue('SBSE_DIGBalance', balance);
$('.DIG__current_balance').attr('data-value', balance);
}
});
});
// bind row event
const $totalAmount = $('.DIG_total_amount');
const getPrice = ($tr) => {
let p = 0;
const $DIGPoints = $tr.find('td:contains( DIG Points)');
if ($DIGPoints.length === 1) p = $DIGPoints.text();
else {
const tds = $tr.children('td').get();
for (let j = tds.length - 1; j >= 0; j -= 1) {
const t = tds[j].textContent.trim();
if (t.startsWith('$')) {
p = t.replace(/\D/g, '');
break;
}
}
}
return parseInt(p, 10);
};
$('a[href^="account_buy"]').eachAsync((ele) => {
const $ele = $(ele);
const $tr = $ele.closest('tr');
const $title = $tr.children('td').eq(pathname.includes('/account_digstore') ? 3 : 1);
const id = $ele.attr('href').replace(/\D/g, '');
const title = $title.text().trim();
const price = getPrice($tr);
const onclickHandler = $tr.attr('onclick');
// setup row data & event
$tr.attr({
'data-id': id,
'data-title': title,
'data-price': price,
});
$tr.addClass('DIG-row').on('click', () => {
$tr.toggleClass('DIG-row--checked');
$totalAmount.attr('data-value', (index, value) => parseInt(value, 10) + (price * ($tr.hasClass('DIG-row--checked') ? 1 : -1)));
});
// re-locate onclick handler
if (pathname.includes('/site_content_marketplace') && onclickHandler) {
$title.wrapInner(
$('<span></span>').attr('onclick', onclickHandler),
);
$tr.removeAttr('onclick');
}
// check if owned
const $a = $tr.find('a[href*="steampowered"]');
const d = {};
let steamID = 0;
if ($a.length === 1) {
const data = $a[0].pathname.slice(1).split('/');
steamID = parseInt(data[1], 10);
d[data[0]] = steamID;
} else if (onclickHandler.includes('site_gamelisting_')) {
steamID = parseInt(onclickHandler.match(/_(\d+)\./)[1], 10);
d.app = steamID;
}
if (steam.isOwned(d)) $tr.addClass('SBSE-item--owned');
if (steam.isWished(d)) $tr.addClass('SBSE-item--wished');
if (steam.isIgnored(d)) $tr.addClass('SBSE-item--ignored');
// 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
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="DIGMenu-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('.DIGMenu-searchResults').find('iframe')
.slideToggle('fast');
});
}
// remove row if manually hid
if (MPHideList.includes(id)) $tr.remove();
else {
// append manual hide feature
$tr.children().eq(0).attr('title', i18n.get('DIGClickToHideThisRow')).click((e) => {
e.stopPropagation();
if (id > 0) {
MPHideList.push(id);
GM_setValue('SBSE_DIGMPHideList', JSON.stringify(MPHideList));
$tr.remove();
}
});
}
}, () => {
swal({
titleText: i18n.get('successTitle'),
text: i18n.get('loadingSuccess'),
type: 'success',
timer: 3000,
});
});
// setup current balance
$('.DIG__current_balance').attr('data-value', GM_getValue('SBSE_DIGBalance', 0));
// extension for creating trade at market place
} else if (pathname === '/site_content_giveaways.html') {
swal.showLoading();
// inject css styles
GM_addStyle(`
body.hideOwned .SBSE-item--owned { display: none; }
.DIGMenu > * { margin-right: 10px; padding: 4px 0 !important; cursor: pointer; }
.DIG-row { height: 30px; }
.SBSE-item--owned .DIG4-Orange-14 { color: #9ccc65; }
.SBSE-item--wished .DIG4-Orange-14 { color: #29b6f6; }
.SBSE-item--ignored .DIG4-Orange-14 { text-decoration: line-through; }
.showOwnedListings { display: inline-block; color: #FD5E0F; }
.showOwnedListings > label { vertical-align: text-bottom; }
.showOwnedListings input:checked + .SBSE-switch__slider { background-color: #FD5E0F; }
`);
// append menu buttons
const $target = $('a[href^="site_content_giveaways_"]').eq(0).closest('table#DIG2TableGray');
const $DIGMenu = $(`
<div class="DIGMenu">
<label class="showOwnedListings">
<label class="SBSE-switch SBSE-switch--small">
<input type="checkbox" id="showOwnedListings" checked>
<span class="SBSE-switch__slider"></span>
</label>
<span>${i18n.get('owned')}</span>
</label>
</div>
`);
$target.before($DIGMenu);
// bind button event
$('.DIGButtonPurchase').click(() => {
let balance = GM_getValue('SBSE_DIGBalance');
const $games = $('.DIG-row--checked:visible');
swal({
title: i18n.get('DIGButtonPurchasing'),
html: '<p></p>',
onOpen: () => {
swal.showLoading();
},
});
(async function purchaseHandler() {
const game = $games.shift();
if (game) {
const $game = $(game);
const id = $game.attr('data-id');
const price = parseInt($game.attr('data-price'), 10);
const title = $game.attr('data-title');
if (title.length > 0) swal.getContent().querySelector('p').textContent = title;
if (id && price > 0) {
if (balance - price >= 0) {
let url = `${location.origin}/account_buy.html`;
const requestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `quantity=1&xgameid=${id}&xgameprice1=${price}&send=Purchase`,
mode: 'same-origin',
credentials: 'same-origin',
cache: 'no-store',
referrer: `${location.origin}/account_buy_${id}.html`,
};
if (pathname === '/account_trades.html' || pathname === '/account_tradesXT.html' || pathname === '/site_content_marketplace.html') {
url = `${location.origin}/account_buytrade_${id}.html`;
requestInit.body = `gameid=${id}&send=Purchase`;
requestInit.referrer = url;
}
const res = await fetch(url, requestInit);
if (res.ok) {
$game.click();
balance -= price;
$('.DIG__current_balance').attr('data-value', balance);
}
purchaseHandler();
} else {
swal({
title: i18n.get('failTitle'),
text: i18n.get('DIGInsufficientFund'),
type: 'error',
});
}
} else purchaseHandler();
} else {
GM_setValue('SBSE_DIGBalance', balance);
swal({
title: i18n.get('successTitle'),
text: i18n.get('DIGFinishedPurchasing'),
type: 'success',
});
}
}());
});
$('#showOwnedListings').on('change', (e) => {
const showOwnedListings = e.delegateTarget.checked;
$('body').toggleClass('hideOwned', !showOwnedListings);
GM_setValue('DIGShowOwnedListings', showOwnedListings);
});
// menu settings
$('#showOwnedListings').prop('checked', GM_getValue('DIGShowOwnedListings', true)).change();
// append sync time and event
const seconds = Math.round((Date.now() - steam.lastSync('library')) / 1000);
$DIGMenu.prepend(`
<span class="DIG4-Gray-13"> ${i18n.get('lastSyncTime').replace('%seconds%', seconds)}</span>
`);
$('a[href^="site_gamelisting_"]').eachAsync((ele) => {
const $ele = $(ele);
const $tr = $ele.closest('tr');
const $title = $tr.children('td').eq(1);
const id = $ele.attr('href').replace(/\D/g, '');
const title = $title.text().trim();
// setup row data & event
$tr.addClass('DIG-row').attr({
'data-id': id,
'data-title': title,
});
// check if owned
const d = {
app: parseInt(id, 10)
};
if (steam.isOwned(d)) $tr.addClass('SBSE-item--owned');
if (steam.isWished(d)) $tr.addClass('SBSE-item--wished');
if (steam.isIgnored(d)) $tr.addClass('SBSE-item--ignored');
}, () => {
swal({
titleText: i18n.get('successTitle'),
text: i18n.get('loadingSuccess'),
type: 'success',
timer: 3000,
});
});
} else if (pathname === '/account_createtrade.html') {
const $form = $('#form_createtrade');
// create trade page
if ($form.length > 0) {
// trim input field
const $gameTitle = $form.find('input[name="typeahead"]');
const $steamKey = $form.find('input[name="STEAMkey"]');
$gameTitle.blur(() => {
unsafeWindow.jQuery('input.typeahead').typeahead('setQuery', $gameTitle.val().trim());
});
$steamKey.blur((e) => {
const $self = $(e.delegateTarget);
const key = $self.val().match(regKey);
if (key) $self.val(key[0]);
});
$steamKey.attr({
size: 50,
maxlength: 200,
});
// search for current market price when click dropdown menu
const $searchResult = $('<div/>');
$gameTitle.closest('table').after($searchResult);
$searchResult.before(`<h3>${i18n.get('DIGMarketSearchResult')}</h3>`);
$('.tt-dropdown-menu').click(async () => {
$searchResult.empty();
const res = await fetch(`${location.origin}/account_tradesXT.html`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `search=${encodeURIComponent($gameTitle.val()).replace(/%20/g, '+')}&button=SEARCH`,
credentials: 'same-origin',
});
const result = res.ok ? $(await res.text()).find('#TableKeys') : 'Network response was not ok.';
$searchResult.append(result);
});
// apply last input price
const lastPrice = GM_getValue('SBSE_DIGLastPrice', 20);
const $priceField = $('input[name=price]');
$priceField.val(lastPrice).trigger('input');
$('#form_createtrade').submit(() => {
const price = parseInt($priceField.val(), 10);
if (price !== lastPrice) GM_setValue('SBSE_DIGLastPrice', price);
});
// result page
} else {
GM_addStyle(`
.check.icon {
width: 42px; height: 24px;
margin: 12px 0 5px 9px;
border-bottom: solid 3px currentColor;
border-left: solid 3px currentColor;
transform: rotate(-45deg);
color: #5cb85c;
}
.remove.icon { color: #d9534f; margin-left: 9px; margin-top: 30px; }
.remove.icon:before, .remove.icon:after {
width: 45px; height: 3px;
position: absolute;
content: '';
background-color: currentColor;
transform: rotate(45deg);
}
.remove.icon:after { transform: rotate(-45deg); }
`);
const $anchor = $('td.DIG3_14_Gray > table:first-child');
const IsSucceed = !!$('td.DIG3_14_Gray:contains("The game key has been added to the DIG MarketPlace.")').length;
if (IsSucceed) $anchor.after('<div class="check icon"></div>');
else $anchor.after('<div class="remove icon"></div>');
}
}
},
ccyyshop() {
// inject css
GM_addStyle(`
.SBSE-container {
width: 80%;
position: relative;
margin: 0 auto;
font-size: 16px;
color: #000;
z-index: 999;
}
.SBSE-container__content__model > textarea {
background-color: #EEE;
box-shadow: 0 0 1px 1px rgba(204,204,204,0.5);
border-radius: 5px;
}
.SBSE-container__content__model > div { text-align: left; }
.SBSE-button {
width: 80px;
border: 1px solid #2e6da4;
border-radius: 5px;
background-color: #337ab7;
color: #FFF;
}
.SBSE-container label { color: #EEE; }
.expanded .showOrderMeta {
display: block !important;
position: absolute;
margin-top: -8px;
right: 265px;
z-index: 1;
}
`);
const handlers = {
extract() {
const data = {
title: 'CCYYCN Bundle',
filename: 'CCYYCN Bundle',
items: [],
};
$('.deliver-gkey > *:contains(-)').each((i, ele) => {
const $ele = $(ele);
const d = {
title: $ele.closest('.deliver-game').prev().text().trim(),
key: $ele.text().trim(),
};
activator.pushKeyDetails(d);
data.items.push(d);
});
return data;
},
reveal(e) {
const $revealBtn = $(e.currentTarget);
const handler = ($games, callback) => {
const game = $games.shift();
if (game) {
game.click();
setTimeout(handler.bind(null, $games, callback), 300);
} else callback();
};
$revealBtn.addClass('SBSE-button--working');
handler($('.deliver-btn'), () => {
$revealBtn.removeClass('SBSE-button--working');
$('.SBSE-button-retrieve').click();
});
},
};
const $container = container.get('SBSE', handlers);
$container.find('.SBSE-select-filter').remove(); // hide filter selector
$container.find('.SBSE-button').addClass('SBSE-button--narrow'); // narrow buttons
// insert textarea
$('.featurette-divider').eq(0).after($container);
},
groupees() {
if (location.pathname.startsWith('/profile/')) {
// inject css
GM_addStyle(`
.SBSE-container__content__model > textarea, .SBSE-button {
background: transparent;
border: 1px solid #8cc53f;
border-radius: 3px;
color: #8cc53f;
transition: all 0.8s ease;
}
.SBSE-button:hover {
background-color: #8cc53f;
color: white;
text-decoration: none;
}
img.product-cover { display: none; }
`);
const handlers = {
extract() {
const bundleTitle = $('h2').text().trim();
const data = {
title: bundleTitle,
filename: `Groupees ${bundleTitle} Keys`,
items: [],
};
$('.key-block input.code').each((i, ele) => {
const $ele = $(ele);
const key = $ele.val();
if (key.includes('-')) {
const $titleEle = $ele.closest('tr').prev().find('td:nth-of-type(3)');
const d = {
title: $titleEle.text().trim(),
key,
used: !!$ele.closest('.key-block').find('.key-status:contains(used)').length,
};
activator.pushKeyDetails(d);
data.items.push(d);
}
});
return data;
},
reveal(e) {
const $revealBtn = $(e.currentTarget);
const handler = ($games, callback) => {
const game = $games.shift();
if (game) {
game.click();
setTimeout(handler.bind(null, $games, callback), 300);
} else callback();
};
$revealBtn.addClass('SBSE-button--working');
const $reveals = $('.product:has(img[title*=Steam]) .reveal-product');
const timer = $reveals.length > 0 ? 1500 : 0;
$reveals.click();
setTimeout(() => {
handler($('.btn-reveal-key'), () => {
$revealBtn.removeClass('SBSE-button--working');
$('.SBSE-button-retrieve').click();
});
}, timer);
},
};
const $container = container.get('SBSE', handlers);
$container.find('.SBSE-select-filter').hide(); // hide filter selector
// append checkbox for used-key
$('.SBSE-button-setting').before(`
<label><input type="checkbox" class="SBSE-checkbox-skipUsed" checked>${i18n.get('checkboxSkipUsed')}</label>
`);
// insert container
$('.table-products').before($container);
// 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',
});
});
// bind custom event
$(document).on('activated', (e, key, result) => {
if (result.success === 1) $(`.btn-steam-redeem[href*=${key}]`).next('.key-usage-toggler').click();
});
} else {
// inject css
GM_addStyle(`
.SBSE-container { margin-bottom: 20px; }
.SBSE-container__content__model > textarea { background-color: #EEE; border-radius: 3px; }
.SBSE-button { outline: none !important; }
.SBSE-button-setting { margin-top: 8px; }
`);
const handlers = {
extract() {
const bundleTitle = $('.expanded .caption').text().trim();
const data = {
title: bundleTitle,
filename: `Groupees ${bundleTitle} Keys`,
items: [],
};
$('.expanded .code').each((i, ele) => {
const $ele = $(ele);
const d = {
title: $ele.closest('.details').find('h3').text().trim(),
key: $ele.val(),
used: $ele.closest('li').find('.usage').prop('checked'),
};
activator.pushKeyDetails(d);
data.items.push(d);
});
return data;
},
reveal(e) {
const $revealBtn = $(e.currentTarget);
const handler = ($games, callback) => {
const game = $games.shift();
if (game) {
game.click();
setTimeout(handler.bind(null, $games, callback), 300);
} else callback();
};
$revealBtn.addClass('SBSE-button--working');
const $reveals = $('.product:has(img[title*=Steam]) .reveal-product');
const timer = $reveals.length > 0 ? 1500 : 0;
$reveals.click();
setTimeout(() => {
handler($('.expanded .reveal'), () => {
$revealBtn.removeClass('SBSE-button--working');
$('.SBSE-button-retrieve').click();
});
}, timer);
},
};
const $container = container.get('SBSE', handlers);
// append checkbox for used-key
$container.find('.SBSE-button-setting').before($(`
<label><input type="checkbox" class="SBSE-checkbox-skipUsed" checked>${i18n.get('checkboxSkipUsed')}</label>
`));
// add buttons style via groupees's class
$container.find('.SBSE-button').addClass('btn btn-default');
// insert container
$('.container > div').eq(1).before($container);
// append mark all as used button
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).forEach((addedNode) => {
const $orderMeta = $(addedNode).find('.order-meta');
if ($orderMeta.length > 0) {
$orderMeta.after(
$(`<button class="btn btn-default" style="margin-right: 10px;"><b>${i18n.get('markAllAsUsed')}</b></button>`).click(() => {
$('.expanded .usage').each((i, checkbox) => {
if (!checkbox.checked) checkbox.click();
});
}),
);
$orderMeta.parent().addClass('showOrderMeta');
}
});
});
}).observe($('#profile_content')[0], {
childList: true
});
// bind custom event
$(document).on('activated', (e, key, result) => {
if (result.success === 1) $(`li.key:has(input[value=${key}]) .usage`).click();
});
}
},
agiso() {
const keys = unique($('body').text().match(regKey));
if (keys.length > 0) {
// inject css
GM_addStyle(`
.SBSE-container__content__model > textarea { border: 1px solid #AAAAAA; }
.SBSE-button {
border: 1px solid #d3d3d3;
background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;
color: #555555;
}
.SBSE-button:hover {
border-color: #999999;
background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;
color: #212121;
}
`);
const handlers = {
extract() {
const bundleTitle = $('a[href*="tradeSnap.htm"]').eq(1).text().trim();
const data = {
title: bundleTitle,
filename: `agiso ${bundleTitle} Keys`,
items: [],
};
keys.forEach((key) => {
data.items.push({
key
});
});
return data;
},
};
const $container = container.get('SBSE', handlers);
$container.find('.SBSE-button-reveal, .SBSE-select-filter').remove();
// insert container
$('#tabs').eq(0).prepend($container);
}
},
keylol() {
if (location.pathname.startsWith('/tooltip')) {
GM_addStyle('body { overflow: hidden; }');
}
},
yuplay() {
// inject css
GM_addStyle(`
.SBSE-container { margin-top: 20px; }
.SBSE-container__content__model > textarea { background-color: rgb(230, 230, 229); color: rgb(27, 26, 26); }
.SBSE-container__content__model > div { text-align: left; }
.SBSE-button {
width: 80px;
border: 1px solid #b4de0a;
background-color: #b4de0a;
color: #1a1a1a;
}
.SBSE-button:hover {
border: 1px solid #a4ca09;
background-color: #a4ca09;
}
.SBSE-container label { color: #1a1a1a; font-weight: 400; }
.SBSE-table-appList { margin-bottom: 10px; }
.SBSE-table-appList td { vertical-align: top; }
.SBSE-table-appList a { display: block; margin-bottom: 5px; }
.SBSE-icon { position: relative; top: 5px; }
`);
const handlers = {
extract() {
const data = {
title: 'Yuplay Games',
filename: 'Yuplay Games',
items: [],
};
$('.product-info').each((i, ele) => {
const $ele = $(ele);
const d = {
title: $ele.find('.name').text().trim(),
key: $ele.next('.keys').find('input').val(),
};
activator.pushKeyDetails(d);
data.items.push(d);
});
return data;
},
};
const appListHandler = (data) => {
if (data.length > 0) {
const $appList = $('<table class="SBSE-table-appList"></table>');
$appList.append('<tr><td colspan="2">App List</td></tr>');
data.forEach((d) => {
const $row = $('<tr/>');
$row.append(
$('<td/>').append($('<span class="SBSE-icon"></span>').mouseenter(keylolTooltip.show.bind(keylolTooltip))),
$(`<td><a href="https://store.steampowered.com/app/${d.app}" target="_blank">${d.title}</a></td>`),
);
d.owned = steam.isOwned(d);
d.wished = steam.isWished(d);
if (d.owned) $row.addClass('SBSE-item--owned');
if (d.wished) $row.addClass('SBSE-item--wished');
$row.addClass('SBSE-item--processed SBSE-item--steam').attr('data-gameinfo', JSON.stringify(d));
$appList.append($row);
});
$('.list-character').after($appList);
// load Keylol tooltip
keylolTooltip.load(data);
}
};
const $container = container.get('SBSE', handlers);
$container.find('.SBSE-button').addClass('SBSE-button--narrow'); // narrow buttons
$container.find('.SBSE-button-reveal, .SBSE-select-filter').remove(); // remove reveal
// insert textarea
$('.table-light').eq(0).before($container);
// append info from SteamDB if found subid
$('.list-character p').each((i, ele) => {
const $ele = $(ele);
const text = $ele.text().trim();
if (text.startsWith('Steam')) {
const subID = text.match(/\d+/)[0];
const steamDBUrl = `https://steamdb.info/sub/${subID}/`;
const steamDBKey = `SBSE_steamDB_sub_${subID}`;
const steamDBData = GM_getValue(steamDBKey, '');
$ele.find('span').replaceWith(`<a href="${steamDBUrl}" target="_blank">${subID}</a>`);
if (steamDBData.length === 0) {
GM_xmlhttpRequest({
url: steamDBUrl,
method: 'GET',
onload(res) {
if (res.status === 200) {
const data = [];
$(res.response).find('#apps .app').each((j, app) => {
const $app = $(app);
const d = {
title: $app.children('td').eq(2).text().trim(),
app: parseInt($app.attr('data-appid'), 10),
};
data.push(d);
});
GM_setValue(steamDBKey, JSON.stringify(data));
appListHandler(data);
}
},
});
} else appListHandler(JSON.parse(steamDBData));
}
});
},
'gama-gama': () => {
// inject css
GM_addStyle(`
.SBSE-container__content__model > textarea { background-color: #ededed; color: #33; border-radius: 4px; }
.SBSE-button {
width: 80px; height: 35px;
border: none; border-radius: 4px;
background: linear-gradient(to bottom, #47bceb 0, #18a4dd 30%, #127ba6 100%);
color: #fff;
box-shadow: 0 1px 3px 1px rgba(0,0,0,.8);
}
.SBSE-button { font-family: inherit; font-size: inherit; }
.SBSE-button:hover { background: linear-gradient(to bottom, #47bceb, #18a4dd); }
`);
const handlers = {
extract() {
const data = {
title: 'Gama Gama Games',
filename: 'Gama Gama Games',
items: [],
};
$('.gift-line').each((i, ele) => {
const $ele = $(ele);
$ele.find('.key-list > li').each((j, key) => {
const d = {
title: $ele.find('.gift-header').text().trim(),
key: key.textContent.trim(),
};
activator.pushKeyDetails(d);
data.items.push(d);
});
});
return data;
},
};
const $container = container.get('SBSE', handlers);
$container.find('.SBSE-button').addClass('SBSE-button--narrow'); // narrow buttons
$container.find('.SBSE-button-reveal, .SBSE-select-filter').remove(); // remove reveal
// insert textarea
$('.user-info').eq(0).after($container);
},
plati() {
let selectedCurrency = GM_getValue('SBSE_selectedCurrency', 'USD');
let platiCurrency = $('th.product-price select option:selected').text().trim();
const plati = {
data: JSON.parse(GM_getValue('SBSE_plati', '{}')),
save(callback) {
GM_setValue('SBSE_plati', JSON.stringify(this.data));
if (typeof callback === 'function') callback();
},
set(key, value, callback) {
this.data[key] = value;
this.save(callback);
},
setItem(id, value, save) {
this.data.itemData[id] = value;
if (save) this.save();
},
get(key) {
return has.call(this.data, key) ? this.data[key] : null;
},
getItem(id) {
return has.call(this.data.itemData, id) ? this.data.itemData[id] : null;
},
init() {
if (!has.call(this.data, 'enablePlatiFeature')) this.data.enablePlatiFeature = true;
if (!has.call(this.data, 'fetchOnStart')) this.data.fetchOnStart = true;
if (!has.call(this.data, 'infiniteScroll')) this.data.infiniteScroll = true;
if (!has.call(this.data, 'itemData')) this.data.itemData = {};
if (!has.call(this.data, 'filterGame')) this.data.filterGame = true;
if (!has.call(this.data, 'filterDLC')) this.data.filterDLC = true;
if (!has.call(this.data, 'filterPackage')) this.data.filterPackage = true;
if (!has.call(this.data, 'filterBundle')) this.data.filterBundle = true;
if (!has.call(this.data, 'filterOwned')) this.data.filterOwned = true;
if (!has.call(this.data, 'filterWished')) this.data.filterWished = true;
if (!has.call(this.data, 'filterIgnored')) this.data.filterIgnored = true;
if (!has.call(this.data, 'filterNotOwned')) this.data.filterNotOwned = true;
if (!has.call(this.data, 'filterNotApplicable')) this.data.filterNotApplicable = true;
if (!has.call(this.data, 'filterNotFetched')) this.data.filterNotFetched = true;
this.save();
},
};
const infiniteScroll = {
enabled: plati.get('infiniteScroll'),
loading: false,
lastPage: 0,
reachedLastPage: false,
pathname: $('head #popup-container + script').text().match(/\/asp\/block_goods.+?\.asp/)[0],
parameters: {
idr: 0,
sort: 'name',
page: 0,
rows: 10,
curr: 'USD',
lang: unsafeWindow.plang || 'en-US',
},
setParameters() {
const $paging = $('.pages_nav').eq(0).children('a');
const onclickArguments = $paging.eq(0).attr('onclick').match(/\((.+)\)/);
if (onclickArguments[1]) {
const parameters = onclickArguments[1].split(',').map(x => (isNaN(x) ? x.replace(/['"]+/g, '') : parseInt(x, 10)));
this.parameters.idr = parameters[0];
this.parameters.sort = parameters[1];
this.parameters.rows = parameters[3];
this.parameters.curr = parameters[4];
this.parameters.page = parseInt($paging.filter('.active').text(), 10) + 1;
this.lastPage = parseInt($paging.filter(':last-child').text(), 10);
}
if (this.pathname) {
const type = this.pathname.slice(-5, -4);
this.parameters[`id_${type}`] = location.pathname.includes('/seller/') ? location.pathname.split('/').pop() : this.parameters.idr;
}
},
fetchNextPage: async function fetchNextPage() {
const $loader = $('.content_center .platiru-loader').eq(0);
$loader.css('visibility', 'visible');
this.loading = true;
const $wrap = $('.SBSE-infiniteScroll-wrap');
const $table = $wrap.find('table.goods-table');
const params = this.parameters;
params.rnd = Math.random();
if (this.pathname) {
const res = await fetch(`${this.pathname}?${$.param(params)}`);
const $resHTML = $(await res.text());
const $trs = $resHTML.find('tbody > tr');
if (res.ok && $trs.length > 0) {
$table.find('tbody').append($trs);
// refresh paging
$wrap.siblings('.pages_nav, .sort_by').remove();
$wrap.after($resHTML.filter('.pages_nav, .sort_by'), $resHTML.find('.goods-table ~ *'));
params.page += 1;
this.reachedLastPage = params.page > this.lastPage;
}
}
this.loading = false;
this.scrollHandler();
$loader.css('visibility', 'hidden');
},
scrollHandler() {
const $wrap = $('.SBSE-infiniteScroll-wrap');
if ($('body').is('.enablePlatiFeature.infiniteScroll') &&
$wrap.length > 0 &&
this.enabled === true &&
this.loading === false &&
this.reachedLastPage === false) {
const spaceTillBotom = $wrap.prop('scrollHeight') - $wrap.scrollTop() - $wrap.height();
if (spaceTillBotom < 200) this.fetchNextPage();
}
},
init() {
if ($('.SBSE-infiniteScroll-wrap').length === 0) {
$('.goods-table').wrap($('<div class="SBSE-infiniteScroll-wrap"></div>').on('scroll', this.scrollHandler.bind(this)));
}
this.scrollHandler();
},
};
const processor = {
fetchItem: async function fetchItem(queue) {
const tr = queue.shift();
if (tr) {
const $tr = $(tr);
const url = $tr.attr('data-url');
const id = parseInt($tr.attr('data-id'), 10);
const classes = ['SBSE-item--fetching', 'SBSE-item--fetched'];
if (url.length > 0 && id > 0) {
const res = await fetch(url);
if (res.ok) {
const itemPageHTML = await res.text();
const description = itemPageHTML.slice(itemPageHTML.indexOf('goods-descr-text'), itemPageHTML.indexOf('goods_reviews'));
const found = description.match(regURL);
if (found) {
const type = found[3].slice(0, 3).toLowerCase();
const steamID = parseInt(found[4], 10);
const item = {};
item[type] = steamID;
plati.setItem(id, item);
if (steam.isOwned(item)) classes.push('SBSE-item--owned');
if (steam.isWished(item)) classes.push('SBSE-item--wished');
if (steam.isIgnored(item)) classes.push('SBSE-item--ignored');
if (classes.length === 1) classes.push('SBSE-item--notOwned');
if (steam.isGame(item)) classes.push('SBSE-item--game');
if (steam.isDLC(item)) classes.push('SBSE-item--DLC');
if (steam.isPackage(item)) classes.push('SBSE-item--package');
} else {
plati.setItem(id, {});
classes.push('SBSE-item--notApplicable');
}
} else classes.push('SBSE-item--failed');
}
$tr.removeClass('SBSE-item--owned SBSE-item--wished SBSE-item--ignored SBSE-item--notOwned SBSE-item--notApplicable');
$tr.toggleClass(classes.join(' '));
this.fetchItem(queue);
} else plati.save();
},
fetchItems(items) {
const filters = ['.SBSE-item--fetching'];
if (plati.get('fetchOnStart')) filters.push('.SBSE-item--fetched');
const $trs = items && items.length > 0 ? $(items) : $('.goods-table tbody > tr');
const $filtered = $trs.filter(`.SBSE-item--steam:not(${filters.join()})`);
$filtered.addClass('SBSE-item--fetching').removeClass('SBSE-item--notFetched');
this.fetchItem($filtered.get());
},
process($rows = null) {
if (plati.get('enablePlatiFeature')) {
const $table = $('.goods-table');
const $trs = $rows && $rows.length > 0 ? $rows : $table.find('tbody > tr');
// setup type & icon node
$trs.find('td:not(.icon) + .product-sold').before(`
<td class="type"><span class="SBSE-type"></span></td>
<td class="icon"><span class="SBSE-icon"></span></td>
`);
// setup price node
$trs.filter(':not(:has(.SBSE-price))').find('.product-price div').each((i, price) => {
const $price = $(price);
const value = parseFloat($price.text().trim()) * 100;
$price.replaceWith(`<span class="SBSE-price" data-currency="${platiCurrency}" data-value="${value}"></span>`);
});
// process
$trs
.filter(':not(.SBSE-item--processing, .SBSE-item--processed)')
.addClass('SBSE-item--processing SBSE-item--steam')
.each((i, tr) => {
const $tr = $(tr);
const url = $tr.find('.product-title a').attr('href');
const id = parseInt(url.split('/').pop(), 10);
if (url.length > 0 && id > 0) {
const classes = [];
const item = plati.getItem(id);
if (item !== null) {
classes.push('SBSE-item--fetched');
if (item.app || item.sub) {
if (steam.isOwned(item)) classes.push('SBSE-item--owned');
if (steam.isWished(item)) classes.push('SBSE-item--wished');
if (steam.isIgnored(item)) classes.push('SBSE-item--ignored');
if (classes.length === 1) classes.push('SBSE-item--notOwned');
if (steam.isGame(item)) classes.push('SBSE-item--game');
if (steam.isDLC(item)) classes.push('SBSE-item--DLC');
if (steam.isPackage(item)) classes.push('SBSE-item--package');
} else classes.push('SBSE-item--notApplicable');
$tr.attr('data-item', JSON.stringify(item));
}
if (classes.length > 0) {
$tr
.removeClass('SBSE-item--owned SBSE-item--wished SBSE-item--ignored SBSE-item--notOwned SBSE-item--notApplicable')
.addClass(classes.join(' '));
} else $tr.addClass('SBSE-item--notFetched');
$tr.attr({
'data-id': id,
'data-url': location.origin + url,
});
}
})
.removeClass('SBSE-item--processing')
.addClass('SBSE-item--processed');
// auto fetch on page visit
if (plati.get('fetchOnStart')) this.fetchItems();
xe.update(selectedCurrency);
}
},
initTable(table) {
const $table = table ? $(table) : $('.goods-table');
const filters = $('.SBSE-plati-menu [data-config^="filter"] input:not(:checked)').map((i, ele) => ele.dataset.filter).get();
platiCurrency = $table.find('th.product-price select option:selected').text().trim();
// apply filters
$table.addClass(filters.join(' '));
// add type & icon
$table.find('thead th:not(.icon) + .product-sold').before('<th class="type"></th><th class="icon"></th>');
// grab infinite scroll parameters
infiniteScroll.setParameters();
// bind infinite scroll event
if (plati.get('infiniteScroll')) infiniteScroll.init();
},
init() {
this.initTable();
this.process();
const self = this;
// detect list changes
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).forEach((addedNode) => {
const $addedNode = $(addedNode);
if ($addedNode.is('.goods-table')) {
self.initTable.call(self, $addedNode);
self.process.call(self, $addedNode.find('tbody tr'));
}
if ($addedNode.is('tr')) {
self.process.call(self, $addedNode);
}
});
});
}).observe($('body')[0], {
childList: true,
subtree: true,
});
},
};
const insertMenu = () => {
const $menu = $(`
<ul class="SBSE-plati-menu">
<li data-config="enablePlatiFeature">
<label class="SBSE-switch SBSE-switch--small">
<input type="checkbox" id="enablePlatiFeature">
<span class="SBSE-switch__slider"></span>
</label>
<label for="enablePlatiFeature"><span>${i18n.get('enablePlatiFeature')}</span></label>
</li>
<li data-config="fetchOnStart">
<label class="SBSE-switch SBSE-switch--small">
<input type="checkbox" id="fetchOnStart">
<span class="SBSE-switch__slider"></span>
</label>
<label for="fetchOnStart"><span>${i18n.get('platiFetchOnStart')}</span></label>
</li>
<li data-config="infiniteScroll">
<label class="SBSE-switch SBSE-switch--small">
<input type="checkbox" id="infiniteScroll">
<span class="SBSE-switch__slider"></span>
</label>
<label for="infiniteScroll"><span>${i18n.get('platiInfiniteScroll')}</span></label>
</li>
<li data-config="fetchButton"><span>${i18n.get('platiFetchButton')}</span></li>
<li data-config="filterType" class="SBSE-dropdown">
<span>${i18n.get('platiFilterType')}</span>
<ul class="SBSE-dropdown__list">
<li><label><input type="checkbox" data-filter="filterGame"><span>${i18n.get('game')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterDLC"><span>${i18n.get('dlc')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterPackage"><span>${i18n.get('package')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterBundle"><span>${i18n.get('bundle')}</span></label></li>
</ul>
</li>
<li data-config="filterStatus" class="SBSE-dropdown">
<span>${i18n.get('platiFilterStatus')}</span>
<ul class="SBSE-dropdown__list">
<li><label><input type="checkbox" data-filter="filterOwned"><span>${i18n.get('owned')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterWished"><span>${i18n.get('wished')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterIgnored"><span>${i18n.get('ignored')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterNotOwned"><span>${i18n.get('notOwned')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterNotApplicable"><span>${i18n.get('notApplicable')}</span></label></li>
<li><label><input type="checkbox" data-filter="filterNotFetched"><span>${i18n.get('notFetched')}</span></label></li>
</ul>
</li>
<li data-config="currency" class="SBSE-dropdown">
<span class="selectedCurrency">${xe.currencies[selectedCurrency][config.get('language')]}</span>
<ul class="SBSE-dropdown__list"></ul>
</li>
<li data-config="syncButton"><span>${i18n.get('settingsSyncLibrary')}</span></li>
</ul>
`);
const $enablePlatiFeature = $menu.find('[data-config="enablePlatiFeature"] input');
const $fetchOnStart = $menu.find('[data-config="fetchOnStart"] input');
const $infiniteScroll = $menu.find('[data-config="infiniteScroll"] input');
const $fetchButton = $menu.find('[data-config="fetchButton"] span');
const $filters = $menu.find('[data-config^="filter"] input');
const $currencyToggler = $menu.find('[data-config="currency"] ul');
const $syncButton = $menu.find('[data-config="syncButton"] span');
// bind event
$enablePlatiFeature.on('change', () => {
const state = $enablePlatiFeature.prop('checked');
plati.set('enablePlatiFeature', state);
$menu.find('li:not([data-config="enablePlatiFeature"])').toggleClass('hide1', !state);
if (state) processor.init();
$('body').toggleClass('enablePlatiFeature', state);
});
$fetchOnStart.on('change', () => {
const state = $fetchOnStart.prop('checked');
plati.set('fetchOnStart', state);
$fetchButton.parent().toggleClass('hide2', state);
});
$infiniteScroll.on('change', () => {
const state = $infiniteScroll.prop('checked');
plati.set('infiniteScroll', state);
infiniteScroll.enabled = state;
$('body').toggleClass('infiniteScroll', state);
// bind infinite scroll event if not already
if (state) infiniteScroll.init();
});
$fetchButton.on('click', processor.fetchItems.bind(processor));
$filters.on('change', (e) => {
const input = e.delegateTarget;
const filter = input.dataset.filter;
const state = input.checked;
plati.set(filter, state);
$('.goods-table').toggleClass(filter, !state);
infiniteScroll.scrollHandler();
});
Object.keys(xe.currencies).forEach((currency) => {
const currencyName = xe.currencies[currency][config.get('language')];
$currencyToggler.append(
$(`<span>${currencyName}</span>`).on('click', () => {
xe.update(currency);
selectedCurrency = currency;
$currencyToggler.prev('.selectedCurrency').text(currencyName);
}),
);
});
$currencyToggler.find('span').wrap('<li></li>');
$syncButton.on('click', () => {
steam.sync([{
key: 'library',
sync: true,
save: true,
notify: true,
callback() {
processor.process($('.goods-table tbody tr.SBSE-item--notOwned')).call(processor);
},
}]);
});
// apply config
$enablePlatiFeature.prop('checked', plati.get('enablePlatiFeature'));
$menu.find('li:not([data-config="enablePlatiFeature"])').toggleClass('hide1', !plati.get('enablePlatiFeature'));
$fetchOnStart.prop('checked', plati.get('fetchOnStart'));
$infiniteScroll.prop('checked', plati.get('infiniteScroll'));
$fetchButton.parent().toggleClass('hide2', plati.get('fetchOnStart'));
$filters.each((i, input) => {
const filter = input.dataset.filter;
const state = plati.get(filter);
input.checked = state;
$('.goods-table').toggleClass(filter, !state);
});
$('body')
.toggleClass('enablePlatiFeature', plati.get('enablePlatiFeature'))
.toggleClass('infiniteScroll', plati.get('infiniteScroll'));
const $target = $('.merchant_products');
if ($target.length === 0) $('.content_center').before($menu);
else $target.eq(0).prepend($menu);
};
plati.init();
// inject css styles
GM_addStyle(`
li[class*="hide"] { display: none; }
.SBSE-plati-menu { display: flex; margin: 10px 0 0 0 !important; list-style: none; }
.SBSE-plati-menu > li { height: 30px; line-height: 30px; padding-right: 30px; }
.SBSE-plati-menu > li > .SBSE-switch { vertical-align: text-bottom; }
.SBSE-plati-menu > li > * { cursor: pointer; }
.SBSE-dropdown__list { width: max-content; z-index: 999; box-shadow: 5px 5px 10px grey; }
.SBSE-dropdown__list li { cursor: default; }
.SBSE-dropdown__list li > label, .SBSE-dropdown__list li > span { width: 100%; display: inline-block; margin: 0 10px; cursor: pointer; text-align: left; }
tr.SBSE-item--processed:hover { background-color: #f3f3f3; }
tr.SBSE-item--processed:hover .product-title > div::after { display: none; }
.filterGame tr.SBSE-item--game,
.filterDLC tr.SBSE-item--DLC,
.filterPackage tr.SBSE-item--package,
.filterBundle tr.SBSE_bundle,
.filterOwned tr.SBSE-item--owned,
.filterWished tr.SBSE-item--wished,
.filterIgnored tr.SBSE-item--ignored,
.filterNotOwned tr.SBSE-item--notOwned,
.filterNotApplicable tr.SBSE-item--notApplicable,
.filterNotFetched tr.SBSE-item--notFetched { display: none; }
body.enablePlatiFeature .content_center { width: initial; }
body.enablePlatiFeature .right_side { display: none; }
body.enablePlatiFeature .goods-table { width: initial; }
body.enablePlatiFeature .product-title > div { max-width: 600px !important; }
body.enablePlatiFeature.infiniteScroll .SBSE-infiniteScroll-wrap {
max-height: 600px;
margin: 10px 0;
overflow: auto;
}
body.enablePlatiFeature.infiniteScroll .goods-table { margin: 0; }
body.enablePlatiFeature.infiniteScroll .goods-table tbody > tr > td:last-child { padding-right: 5px; }
.SBSE-icon { vertical-align: middle; }
body:not(.enablePlatiFeature) .type,
body:not(.enablePlatiFeature) .icon { display: none; }
.merchant_products > .SBSE-plati-menu { margin: 0 0 10px 0 !important; }
`);
if (location.pathname.startsWith('/seller/') || location.pathname.startsWith('/cat/')) {
insertMenu();
processor.init();
}
},
};
const init = () => {
config.init();
i18n.init();
xe.init();
steam.init();
settings.init();
SBSE.init();
ASF.init();
container.init();
if (location.hostname === 'store.steampowered.com') {
// save sessionID
if (unsafeWindow.g_AccountID > 0) {
const currentID = config.get('sessionID');
const sessionID = unsafeWindow.g_sessionID || '';
const language = unsafeWindow.g_oSuggestParams.l || 'english';
if (!config.get('language')) config.set('language', language);
if (sessionID.length > 0) {
const update = config.get('autoUpdateSessionID') && currentID !== sessionID;
if (!currentID || update) {
config.set('sessionID', sessionID, () => {
swal({
title: i18n.get('updateSuccessTitle'),
text: i18n.get('updateSuccess'),
type: 'success',
timer: 3000,
});
});
}
}
}
} else {
const site = location.hostname.replace(/(www|alds|bundle|steamdb)\./, '').split('.').shift();
// check sessionID
if (!config.get('sessionID')) steam.getSessionID();
if (has.call(siteHandlers, site)) siteHandlers[site](true);
}
keylolTooltip.listen();
};
$(init);