GOG.com - Sale Helper

A simple helper to make GOG.com's grid-view catalogue listings more scannable during a sale

// ==UserScript==
// @name     GOG.com - Sale Helper
// @version  2
// @namespace ssokolow.com
// @description A simple helper to make GOG.com's grid-view catalogue listings more scannable during a sale
//
// @compatible firefox Tested under Greasemonkey 4 and ViolentMonkey
// @compatible chrome Tested under TamperMonkey
//
// @include  https://www.gog.com/games?*
//
// @require  https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require  https://openuserjs.org/src/libs/sizzle/GM_config.js
//
// @grant    GM_registerMenuCommand
// @grant    GM.registerMenuCommand
//
// @grant    GM_getValue
// @grant    GM_setValue
// NOTE: GM_config doesn't currently support GM4 APIs, so allowing the GM4
//       versions of these isn't helpful and could result in users losing
//       access to their settings if GM_config suddenly starts supporting
//       them without transparently migrating data from its localStorage
//       fallback.
// ==/UserScript==
/* jshint esversion: 6 */

var fieldDefs = {
  'discount_min': {
    'section': 'Minimum Discount Percentage',
    'label': 'Minimum',
    'labelPos': 'left',
    'type': 'number',
    'min': 0,
    'max': 99,
    'default': 50,
    'tail': '%',
  },
  'discount_preferred': {
    'label': 'Preferred',
    'type': 'number',
    'min': 0,
    'max': 99,
    'default': 75,
    'tail': '%',
  },
  'discount_preferred_fgcolor': {
    'label': 'Foreground',
    'type': 'color',
    'default': '#ffffff',
  },
  'discount_preferred_bgcolor': {
    'label': 'Background',
    'type': 'color',
    'default': '#00aa00',
  },
  'discount_rare': {
    'label': 'Rare Bargain',
    'type': 'number',
    'min': 0,
    'max': 99,
    'default': 76,
    'tail': '%',
  },
  'discount_rare_fgcolor': {
    'label': 'Foreground',
    'type': 'color',
    'default': '#ffffff',
  },
  'discount_rare_bgcolor': {
    'label': 'Background',
    'type': 'color',
    'default': '#ff0000',
  },
  'price_max': {
    'section': 'Maximum Price',
    'label': 'Maximum',
    'labelPos': 'left',
    'type': 'number',
    'min': '0',
    'step': '0.01',
    'default': '15.00',
  },
  'price_low': {
    'label': 'Preferred',
    'type': 'number',
    'min': '0',
    'step': '0.01',
    'default': '5.00',
  },
  'price_low_fgcolor': {
    'label': 'Foreground',
    'type': 'color',
    'default': '#ffffff',
  },
  'price_low_bgcolor': {
    'label': 'Background',
    'type': 'color',
    'default': '#00aa00',
  },
  'price_impulse': {
    'label': 'Impulse Buys',
    'type': 'number',
    'min': '0',
    'step': '0.01',
    'default': '2.00',
  },
  'price_impulse_fgcolor': {
    'label': 'Foreground',
    'type': 'color',
    'default': '#ffffff',
  },
  'price_impulse_bgcolor': {
    'label': 'Background',
    'type': 'color',
    'default': '#ff0000',
  },
  'hide_owned': {
    'section': 'Other Filters',
    'label': 'Hide entries marked "In Library"',
    'labelPos': 'left',
    'type': 'checkbox',
    'default': true,
  },
  'hide_title_regex': {

    'label': 'Hide entries with titles matching:',
    'labelPos': 'left',
    'type': 'text',
    'size': 35,
    'default': "(Upgrade|[ ]OST|Soundtrack|Artbook)$",
  },
};

function remove_tile(mutation_event) {
  mutation_event.target.closest(".product-tile").remove();
}

let observer = new MutationObserver(function (mutations) {
  mutations.forEach(function (mut) {
    if (mut.type == "attributes" && mut.attributeName == "class" &&
        mut.target.classList.contains("product-tile__labels--in-library")) {
      // Hide entries that are already owned
    	remove_tile(mut);

    } else if (mut.type == "childList" && mut.target.classList.contains("product-tile__discount")) {
      // Hide entries that are below the minimum discount
      // and highlight ones with abnormally high discounts
      let discount = mut.target.innerText;
      let discount_parsed = Number.parseInt(discount.substring(1, discount.length - 1));
      if (discount_parsed < GM_config.get('discount_min')) {
        remove_tile(mut);
      } else if (discount_parsed >= GM_config.get('discount_rare')) {
        mut.target.style.color = GM_config.get('discount_rare_fgcolor');
        mut.target.style.backgroundColor = GM_config.get('discount_rare_bgcolor');
      } else if (discount_parsed >= GM_config.get('discount_preferred')) {
        mut.target.style.color = GM_config.get('discount_preferred_fgcolor');
        mut.target.style.backgroundColor = GM_config.get('discount_preferred_bgcolor');
      }
    } else if (mut.type == "childList" && mut.target.classList.contains("product-tile__prices")) {
      // Hide entries that are over the maximum price
      // and highlight ones at or below the preferred price
      let discounted = mut.target.querySelector(".product-tile__price-discounted");
      let price = Number.parseFloat(discounted.innerText);

      if (price > GM_config.get('price_max')) {
        remove_tile(mut);
      } else if (price <= GM_config.get('price_impulse')) {
        discounted.style.color = GM_config.get('price_impulse_fgcolor');
        discounted.style.backgroundColor = GM_config.get('price_impulse_bgcolor');
      } else if (price <= GM_config.get('price_low')) {
        discounted.style.color = GM_config.get('price_low_fgcolor');
        discounted.style.backgroundColor = GM_config.get('price_low_bgcolor');
      }

      discounted.style.padding = '2px';
      discounted.style.borderRadius = '2px';

    } else if (mut.type == "childList" && mut.target.classList.contains("product-tile__title")) {
      let regex_str = GM_config.get('hide_title_regex');
      if (regex_str && regex_str.trim() && RegExp(regex_str).test(mut.target.innerText)) {
        remove_tile(mut);
      }
    }

  });
});

let frame = document.createElement('div');
document.body.append(frame);

GM_config.init({
  id: 'sale_filter_GM_config',
  title: "Sale Helper Settings",
  fields: fieldDefs,
  css: ('#sale_filter_GM_config ' + [
    	// Match GOG.com styling more closely
    	"{ box-shadow: 0 0 15px rgba(0,0,0,.15),0 1px 3px rgba(0,0,0,.15); " +
       " background: #ccc; color: #212121; border: 0 !important; " +
       " height: auto !important; width: auto !important; margin: auto; padding: 1ex !important; }",
      " * { font-family: Lato GOG,Lato GOG Latin,sans-serif; }",
      ".config_header, .section_header { font-weight: 700; margin: 1ex 0 -1ex; }",
    	".section_header { color: #212121; background-color: inherit; border-width: 0 0 1px 0; " +
       " font-size: 16px; border-bottom: 1px solid #bfbfbf; " +
       " margin: 1em 1em 1ex 1em; text-align: left !important; " +
    	 " clear: both; }",
    	" .title_underline { display: inline-block; border-bottom: 1px solid #212121; padding: 5px 0; }",
    	".field_label { display: inline-block; min-width: 9em; text-align: right; }",
      ".field_label, .field_tail { font-size: 12px; }",
    	".field_tail { margin-left: 0.5ex; font-weight: 700; }",
    	".config_var { float: left; margin-right: 1ex; }",
    	".reset { margin-right: 12px; }",
    	"button, input, .field_tail { vertical-align: middle; }",
    	"input[type='checkbox'] { margin: 0 0.5ex 0 2.5ex; }",
    	"input[type='number'] { width: 5em; }",
    	"input[type='color'] { box-sizing: content-box; height: 1.1em; width: 2em; }",
    	"#sale_filter_GM_config_field_discount_min, #sale_filter_GM_config_field_discount_preferred, " +
       " #sale_filter_GM_config_field_discount_rare { width: 4em !important; }",
    	"#sale_filter_GM_config_buttons_holder { padding-top: 1ex; }",
    	"#sale_filter_GM_config_hide_title_regex_var, #sale_filter_GM_config_hide_owned_var " +
       " { margin-left: 3ex !important; }",
    	"#sale_filter_GM_config_field_hide_owned { margin-left: 1ex; }",
    	"#sale_filter_GM_config_discount_min_var, #sale_filter_GM_config_discount_preferred_var, " +
       " #sale_filter_GM_config_discount_rare_var, #sale_filter_GM_config_price_impulse_var, " +
       " #sale_filter_GM_config_price_low_var, #sale_filter_GM_config_price_max_var, " +
       " #sale_filter_GM_config_hide_title_regex_var, #sale_filter_GM_config_buttons_holder " +
       " { clear: left; }",
    ].join('\n#sale_filter_GM_config ')),
  events: {
    open: function(doc) {
      this.frame.querySelectorAll('.section_header').forEach(function(node) {
        let inner = document.createElement('div');
        inner.classList.add("title_underline");

        inner.innerText = node.innerText;
        node.firstChild.replaceWith(inner);
      });
    },
  },
  types: {
    'color': {
      default: '#ffffff',
      toNode: function(configId) {
        var field = this.settings,
            id = this.id,
            value = this.value,
            create = this.create,
            retNode = create('div', { className: 'config_var',
              id: configId + '_' + id + '_var',
              title: field.title || '' });

        // Create the field lable
        retNode.appendChild(create('label', {
          innerHTML: field.label,
          id: configId + '_' + id + '_field_label',
          for: configId + '_field_' + id,
          className: 'field_label'
        }));

        // Actually create and append the input element
        retNode.appendChild(create('input', {
            id: configId + '_field_' + id,
            type: 'color',
            value: value ? value : this['default'],
        }));

        return retNode;
      },
      toValue: function() {
        if (this.wrapper) {
          return this.wrapper.getElementsByTagName('input')[0].value;
        }
      },
      reset: function() {
        if (this.wrapper) {
          this.wrapper.getElementsByTagName('input')[0].value = this['default'];
        }
      }
    },
    'number': {
      default: '0',
      toNode: function() {
        console.log(this);
        var field = this.settings,
            id = this.id,
            value = this.value,
            create = this.create,
            configId = this.configId,
            retNode = create('div', { className: 'config_var',
              id: configId + '_' + id + '_var',
              title: field.title || '' });

        // Create the field label
        retNode.appendChild(create('label', {
          innerHTML: field.label,
          id: configId + '_' + id + '_field_label',
          for: configId + '_field_' + id,
          className: 'field_label'
        }));

        let params = {
            id: configId + '_field_' + id,
            type: 'number',
            value: value ? value : this['default'],
        };
        if (field.min || field.min === 0) { params.min = field.min; }
        if (field.max || field.max === 0) { params.max = field.max; }
        if (field.step) { params.step = field.step; }

        // Actually create and append the input element
        retNode.appendChild(create('input', params));

        if (field.tail) {
          retNode.appendChild(create('span', {
            innerHTML: field.tail ,
            className: 'field_tail',
          }));
        }
        
        return retNode;
      },
      toValue: function() {
        if (this.wrapper) {
          let val = Number(this.wrapper.getElementsByTagName('input')[0].value);
          if (isNaN(val)) { return null; }
          return val;
        }
      },
      reset: function() {
        if (this.wrapper) {
          this.wrapper.getElementsByTagName('input')[0].value = this['default'];
        }
      }
    }
  },
  frame: frame,
});
GM.registerMenuCommand("Configure Sale Helper...",
                       function() { GM_config.open(); }, 'C');


let catalog = document.querySelector(".catalog__wrapper");
observer.observe(catalog, { attributes: GM_config.get('hide_owned'), childList: true, subtree: true });