NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Humble Enhancer // @author Nathaniel Watson <nathaniel@nwatson.nz> (http://nwatson.nz) // @namespace http://www.nwatson.nz // @description Enhances Humble Bundle pages with additional info // @license https://github.com/nathanielw/humble-enhancer/blob/master/LICENSE // @supportURL https://github.com/nathanielw/humble-enhancer/issues // @homepageURL https://github.com/nathanielw/humble-enhancer // @source https://github.com/nathanielw/humble-enhancer // @compatible firefox // @compatible chrome // @include https://www.humblebundle.com/* // @exclude https://www.humblebundle.com/books* // @exclude https://www.humblebundle.com/monthly* // @exclude https://www.humblebundle.com/store* // @version 0.3.0 // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_getResourceText // @grant GM_addStyle // @connect humblebundle.com // @connect cheapshark.com // @connect www.cheapshark.com // @resource style https://cdn.rawgit.com/nathanielw/humble-enhancer/v0.3.0/dist/style.css // ==/UserScript== (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); Object.defineProperty(exports, "__esModule", { value: true }); var _quantizer = require('./quantizer'); var _quantizer2 = _interopRequireDefault(_quantizer); var _cheapshark = require('./cheapshark'); var Cheapshark = _interopRequireWildcard(_cheapshark); var _htmlEscape = require('./util/html-escape'); var _htmlEscape2 = _interopRequireDefault(_htmlEscape); var _constants = require('./constants'); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var scoreQuantizer = new _quantizer2.default(40, 99, ['lowest', 'low', 'medium', 'high', 'highest']); var BundleGameView = function () { function BundleGameView(element, model) { _classCallCheck(this, BundleGameView); this.element = element; this.model = model; } _createClass(BundleGameView, [{ key: 'displayInfo', value: function displayInfo(storeManager) { var _this = this; var outer = this.element.querySelector('.table-wrapper'); if (outer) { (function () { var buttonContainer = document.createElement('div'); buttonContainer.className = 'he-button-container'; buttonContainer.innerHTML = '\n\t\t\t\t<div class=\'he-button-container__group he-button-container__group--left\'>\n\t\t\t\t\t' + (_this.model.info.metacriticScore && _this.model.info.metacriticLink ? '<a class=\'he-info-button tooltip-top he-metacritic he-metacritic--' + scoreQuantizer.quantize(_this.model.info.metacriticScore) + '\'\n\t\t\t\t\t\t\tdata-tooltip=\'Metacritic score for ' + (0, _htmlEscape2.default)(_this.model.info.external) + '\'\n\t\t\t\t\t\t\thref=\'http://www.metacritic.com' + _this.model.info.metacriticLink + '\'>\n\t\t\t\t\t\t' + _this.model.info.metacriticScore + '\n\t\t\t\t\t\t</a>' : '') + '\n\t\t\t\t</div>\n\t\t\t\t<div class=\'he-button-container__group he-button-container__group--right\'>\n\t\t\t\t\t' + (_this.model.steamLink ? '<a class=\'he-info-button\' href=\'' + _this.model.steamLink + '\'>\n\t\t\t\t\t\t\t<i class=\'fa fa-steam\'></i>\n\t\t\t\t\t\t</a>' : '') + '\n\t\t\t\t\t<span class=\'he-info-button he-info-button--price he-tooltipped\'>\n\t\t\t\t\t\t<i class=\'fa fa-usd\'></i>\n\t\t\t\t\t\t<div class=\'he-tooltip he-tooltip--medium\'>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</span>\n\t\t\t\t</div>'; var priceButton = buttonContainer.querySelector('.he-info-button--price'); var priceTooltip = priceButton.querySelector('.he-tooltip'); if (_this.model.deals) { _this._createTooltip(priceTooltip, storeManager); } else { (function () { var hoverHandler = function hoverHandler() { priceButton.removeEventListener('mouseover', hoverHandler); priceTooltip.innerHTML = '<div class="he-tooltip__inner">Loading deals...</div>'; Cheapshark.deals({ title: _this.model.info.external, exact: 1 }, function (err, dealsData) { if (!err && dealsData.length > 0) { _this.model.deals = dealsData; _this._createTooltip(priceTooltip, storeManager); } else { priceTooltip.innerHTML = '<div class="he-tooltip__inner">No deals found</div>'; } }); }; priceButton.addEventListener('mouseover', hoverHandler); })(); } outer.insertBefore(buttonContainer, outer.firstChild); })(); } } }, { key: '_createTooltip', value: function _createTooltip(tooltipEl, storeManager) { var self = this; tooltipEl.innerHTML = '\n\t\t\t<div class=\'he-tooltip__inner\'>\n\t\t\t\t<h3 class=\'he-tooltip__title\'>External Prices</h3>\n\t\t\t\t<span class=\'he-tooltip__subtitle\'>' + this.model.info.external + '</span>\n\t\t\t</div>\n\t\t\t<div class=\'he-price-list\'>\n\t\t\t\t' + function () { var list = ''; for (var i = 0; i < self.model.deals.length; i++) { var deal = self.model.deals[i]; var store = storeManager.getStore(parseInt(deal.storeID)); if (store) { list += '\n\t\t\t\t\t\t\t\t<a class=\'he-price-list__item\' href=' + (_constants.DEAL_BASE + deal.dealID) + '>\n\t\t\t\t\t\t\t\t\t<span class=\'he-price-list__cell\'>\n\t\t\t\t\t\t\t\t\t\t<img src=\'//cheapshark.com/' + (0, _htmlEscape2.default)(store.images.icon) + '\' />\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<span class=\'he-price-list__cell\'>\n\t\t\t\t\t\t\t\t\t\t' + (0, _htmlEscape2.default)(store.storeName) + '\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<span class=\'he-price-list__cell he-price-list__cell--right\'>\n\t\t\t\t\t\t\t\t\t\t$' + deal.salePrice + '\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</a>'; } } return list; }() + '\n\t\t\t</div>\n\t\t'; } }]); return BundleGameView; }(); exports.default = BundleGameView; },{"./cheapshark":3,"./constants":4,"./quantizer":7,"./util/html-escape":9}],2:[function(require,module,exports){ 'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getValue = getValue; exports.setValue = setValue; exports.clearExpired = clearExpired; exports.clearAll = clearAll; exports.getLastCleared = getLastCleared; var EXPIRY_PREFIX = 'expiry'; var LAST_CLEARED_KEY = 'lastcleared'; function getValue(type, id) { var value = GM_getValue(type + ':' + id); try { return JSON.parse(value); } catch (ex) { return value; } } function setValue(type, id, value, expiry) { if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null) { value = JSON.stringify(value); } GM_setValue(formatKey(type, id), value); if (expiry !== undefined) { GM_setValue(formatKey(EXPIRY_PREFIX, formatKey(type, id)), expiry); } } function clearExpired() { var keys = GM_listValues(); var now = Date.now(); GM_setValue(LAST_CLEARED_KEY, now); keys.forEach(function (key) { if (key.lastIndexOf(EXPIRY_PREFIX, 0) === 0) { var expiry = GM_getValue(key); if (expiry <= now) { var mainKey = expiryKeyToKey(key); GM_deleteValue(mainKey); } } }); } function clearAll() { GM_listValues().forEach(function (key) { GM_deleteValue(key); }); } function getLastCleared() { var time = GM_getValue(LAST_CLEARED_KEY); if (time === undefined) { time = Date.now(); GM_setValue(LAST_CLEARED_KEY, time); } return time; } function formatKey(type, id) { return type + ':' + id; } function expiryKeyToKey(expiryKey) { return expiryKey.substring(EXPIRY_PREFIX.length + 1); } },{}],3:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.games = games; exports.deals = deals; exports.stores = stores; var _networkUtil = require('./util/network-util'); var net = _interopRequireWildcard(_networkUtil); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } var API_URL = 'http://www.cheapshark.com/api/1.0/'; function games(opts, callback) { sendGet('games', opts, callback); } function deals(opts, callback) { sendGet('deals', opts, callback); } function stores(callback) { sendGet('stores', null, callback); } function sendGet(endpoint, opts, callback) { net.httpGet(API_URL + endpoint, opts, responseHandler(callback), responseHandler(callback)); } function responseHandler(callback) { return function (err, response) { if (err) { callback(err, null); } else { var data = JSON.parse(response); callback(null, data); } }; } },{"./util/network-util":10}],4:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var SECONDS = exports.SECONDS = 1000; var MINUTES = exports.MINUTES = 60000; var HOURS = exports.HOURS = 3600000; var DAYS = exports.DAYS = 8640000; var DEAL_BASE = exports.DEAL_BASE = 'http://www.cheapshark.com/redirect.php?dealID='; },{}],5:[function(require,module,exports){ 'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); Object.defineProperty(exports, "__esModule", { value: true }); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var TITLE_REGEX = /[^a-zA-Z0-9\s]/g; var STEAM_BASE = 'http://store.steampowered.com/app/'; var Game = function () { function Game(title) { _classCallCheck(this, Game); this.title = title; this.info = {}; } _createClass(Game, [{ key: 'setInfo', value: function setInfo(info) { for (var attr in info) { this.info[attr] = info[attr]; } } }, { key: 'setMetacriticInfo', value: function setMetacriticInfo(score, link) { if (typeof score !== 'number') { throw Error('Invalid type: Metacritic score must be a number'); } this.info.metacriticScore = score; this.info.metacriticLink = link; } }, { key: 'id', get: function get() { return this.title.replace(TITLE_REGEX, ''); } }, { key: 'deals', set: function set(deals) { this._deals = deals.sort(function (a, b) { return parseFloat(a.salePrice) - parseFloat(b.salePrice); }); }, get: function get() { return this._deals; } }, { key: 'steamLink', set: function set(steamLink) { this.info.steamLink = steamLink; }, get: function get() { if (this.info.steamAppID != null) { return STEAM_BASE + this.info.steamAppID; } else { return this.info.steamLink; } } }]); return Game; }(); exports.default = Game; },{}],6:[function(require,module,exports){ 'use strict'; var _cheapshark = require('./cheapshark'); var Cheapshark = _interopRequireWildcard(_cheapshark); var _cache = require('./cache'); var Cache = _interopRequireWildcard(_cache); var _game = require('./game'); var _game2 = _interopRequireDefault(_game); var _bundleGameView = require('./bundle-game-view'); var _bundleGameView2 = _interopRequireDefault(_bundleGameView); var _storeManager = require('./store-manager'); var _storeManager2 = _interopRequireDefault(_storeManager); var _constants = require('./constants'); var Constants = _interopRequireWildcard(_constants); var _updateTasks = require('./util/update-tasks'); var _updateTasks2 = _interopRequireDefault(_updateTasks); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } var TYPE_GAME = 'game'; function main() { (0, _updateTasks2.default)(); cleanCache(); GM_addStyle(GM_getResourceText('style')); var stores = new _storeManager2.default(); stores.init(initGameInfo); } function initGameInfo(storeManager) { var games = findBundleGames(); var expiry = Date.now() + Constants.DAYS * 14; games.forEach(function (g) { var gameInfo = Cache.getValue(TYPE_GAME, g.model.id); if (gameInfo !== undefined) { if (gameInfo !== null) { g.model.setInfo(gameInfo); g.displayInfo(storeManager); } } else { Cheapshark.games({ title: g.model.title, limit: 10, exact: 1 }, function (err, gamesData) { if (!err) { if (gamesData.length > 0) { g.model.setInfo(gamesData[0]); Cheapshark.deals({ title: g.model.info.external, exact: 1 }, function (err, dealsData) { if (!err && dealsData.length > 0) { g.model.deals = dealsData; var hasSteam = g.model.info.steamAppID != null; var hasMetacritic = false; for (var i = 0; i < dealsData.length && !(hasSteam && hasMetacritic); i++) { var deal = dealsData[i]; if (!hasMetacritic && deal.metacriticLink && deal.metacriticScore) { g.model.setMetacriticInfo(parseInt(deal.metacriticScore), deal.metacriticLink); hasMetacritic = true; } if (!hasSteam && deal.storeID == 1) { g.model.steamLink = Constants.DEAL_BASE + deal.dealID; hasSteam = true; } } Cache.setValue(TYPE_GAME, g.model.id, g.model.info, expiry); } else { g.model.deals = []; } g.displayInfo(storeManager); }); } else { Cache.setValue(TYPE_GAME, g.model.id, null, expiry); } } }); } }); } function cleanCache() { var now = Date.now(); if (now - Cache.getLastCleared() >= Constants.DAYS) { Cache.clearExpired(); } } function findBundleGames() { var gameEls = [].concat(_toConsumableArray(document.querySelectorAll('.promo-body .game-border:not(.charity) .game-boxes > li'))); var gameList = gameEls.map(function (gameEl) { var titleEl = gameEl.querySelector('.game-description h2'); if (titleEl !== null) { var title = titleEl.textContent.trim(); var game = new _game2.default(title); return new _bundleGameView2.default(gameEl, game); } else { return null; } }).filter(function (game) { return game !== null; }); return gameList; } main(); },{"./bundle-game-view":1,"./cache":2,"./cheapshark":3,"./constants":4,"./game":5,"./store-manager":8,"./util/update-tasks":11}],7:[function(require,module,exports){ 'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); Object.defineProperty(exports, "__esModule", { value: true }); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Quantizer = function () { function Quantizer(low, high, values) { _classCallCheck(this, Quantizer); this.low = low; this.high = high; this.values = values; } _createClass(Quantizer, [{ key: 'quantize', value: function quantize(value) { var maxIndex = this.values.length - 1; var ratio = (value - this.low) / (this.high - this.low); var index = Math.ceil(ratio * this.values.length) - 1; index = Math.max(Math.min(index, maxIndex), 0); return this.values[index]; } }]); return Quantizer; }(); exports.default = Quantizer; },{}],8:[function(require,module,exports){ 'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); Object.defineProperty(exports, "__esModule", { value: true }); var _cheapshark = require('./cheapshark'); var Cheapshark = _interopRequireWildcard(_cheapshark); var _cache = require('./cache'); var Cache = _interopRequireWildcard(_cache); var _constants = require('./constants'); var Constants = _interopRequireWildcard(_constants); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var TYPE_STORES = 'stores'; var STORES_KEY = 'all-stores'; var StoreManager = function () { function StoreManager() { _classCallCheck(this, StoreManager); } _createClass(StoreManager, [{ key: 'init', value: function init(callback) { var _this = this; var stores = Cache.getValue(TYPE_STORES, STORES_KEY); var expiry = Date.now() + Constants.DAYS * 14; if (stores !== undefined) { this._setStores(stores); callback(this); } else { Cheapshark.stores(function (err, data) { if (!err) { Cache.setValue(TYPE_STORES, STORES_KEY, data, expiry); _this._setStores(data); } else { _this._setStores([]); } callback(_this); }); } } }, { key: '_setStores', value: function _setStores(stores) { var _this2 = this; this._storeDict = {}; stores.forEach(function (store) { _this2._storeDict[store.storeID] = store; }); } }, { key: 'getStore', value: function getStore(id) { if (this._storeDict === undefined) { throw Error('Store manager has not been initialised.'); } return this._storeDict[id]; } }]); return StoreManager; }(); exports.default = StoreManager; },{"./cache":2,"./cheapshark":3,"./constants":4}],9:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = htmlEscape; function htmlEscape(str) { return str.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/`/g, '`'); } },{}],10:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.httpGet = httpGet; exports.queryStringify = queryStringify; function httpGet(url, params, callback) { GM_xmlhttpRequest({ url: url + '?' + queryStringify(params), method: 'GET', onload: responseHandler(callback), onerror: responseHandler(callback, true) }); } function queryStringify(params) { var queryString = ''; if (params) { queryString = Object.keys(params).reduce(function (a, k) { a.push(k + '=' + encodeURIComponent(params[k])); return a; }, []).join('&'); } return queryString; } function responseHandler(callback, wasError) { return function (response) { if (wasError || response.status !== 200) { callback(new Error('Error communicating with server. ' + response.status + ' ' + response.statusText), null); } else { callback(null, response.responseText); } }; } },{}],11:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = updateTasks; var _cache = require('../cache'); var Cache = _interopRequireWildcard(_cache); var _versionUtil = require('./version-util'); var VersionUtil = _interopRequireWildcard(_versionUtil); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } var TYPE_VERSION = 'version'; var ID_VERSION = 'installed'; var VERSION = '0.3.0'; function updateTasks() { var oldVersion = Cache.getValue(TYPE_VERSION, ID_VERSION); if (!oldVersion) { performTasks(); } else { var oldMajor = VersionUtil.getMajorVersion(oldVersion); var newMajor = VersionUtil.getMajorVersion(VERSION); var oldMinor = VersionUtil.getMinorVersion(oldVersion); var newMinor = VersionUtil.getMinorVersion(VERSION); if (newMajor !== oldMajor || newMajor === 0 && oldMinor !== newMinor) { performTasks(); } } } function performTasks() { Cache.clearAll(); Cache.setValue(TYPE_VERSION, ID_VERSION, VERSION); } },{"../cache":2,"./version-util":12}],12:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.getMajorVersion = getMajorVersion; exports.getMinorVersion = getMinorVersion; function getMajorVersion(verString) { return parseInt(verString.split('.')[0]); } function getMinorVersion(verString) { return parseInt(verString.split('.')[1]); } },{}]},{},[6]);