GOG.com - Promo Filter

Hide/remove games matching your keywords on GOG.com and collapse the grid slots they occupied

// ==UserScript==
// @name         GOG.com - Promo Filter
// @namespace    GOG.com - Promo Filter
// @version      2.4
// @description  Hide/remove games matching your keywords on GOG.com and collapse the grid slots they occupied
// @author       masterofobzene
// @homepage     https://github.com/masterofobzene/UserScriptRepo
// @icon         https://www.gog.com/favicon.ico
// @match        https://www.gog.com/*/games*
// @grant        none
// @run-at       document-end
// @license      GNU GPLv3
// ==/UserScript==

(function() {
  'use strict';

  // — your forbidden words (case‐insensitive) —
  const filterWords   = ['bundle', 'edition', 'supporter', 'halloween', 'christmas', 'soundtrack'];
  const hideMethod    = 'remove';  // 'remove' or 'hide'
  const checkInterval = 1000;       // ms

  // core filter routine
  function filterGames() {
    document
      .querySelectorAll('.product-tile:not([data-filtered])')
      .forEach(tile => {
        const titleEl = tile.querySelector('.product-tile__title, .product-tile__info__name');
        if (!titleEl) return;
        const title = titleEl.textContent.trim().toLowerCase();
        if (filterWords.some(w => title.includes(w))) {
          tile.setAttribute('data-filtered', 'true');

          // climb up until our parent is the real grid/flex container
          let wrapper = tile;
          while (
            wrapper.parentElement &&
            !['grid', 'flex'].includes(
              window.getComputedStyle(wrapper.parentElement).display
            )
          ) {
            wrapper = wrapper.parentElement;
          }

          // if we found that container, remove/hide the grid‐item itself
          if (
            wrapper.parentElement &&
            ['grid', 'flex'].includes(
              window.getComputedStyle(wrapper.parentElement).display
            )
          ) {
            if (hideMethod === 'remove') {
              wrapper.remove();
            } else {
              wrapper.style.display = 'none';
            }
          } else {
            // fallback: hide the tile itself
            tile.style.display = 'none';
          }
        }
      });
  }

  // set up observers & intervals
  function init() {
    filterGames();

    // watch for new tiles (infinite scroll, dynamic re-render)
    const obs = new MutationObserver(muts => {
      if (muts.some(m => m.addedNodes.length > 0)) {
        filterGames();
      }
    });
    obs.observe(document.body, { childList: true, subtree: true });

    // belt‐and‐suspenders: interval re-check
    setInterval(filterGames, checkInterval);

    // clean up on page unload
    window.addEventListener('beforeunload', () => obs.disconnect());
  }

  // **bootstrapping**: wait until at least one .product-tile exists
  const bootstrap = setInterval(() => {
    if (document.querySelector('.product-tile')) {
      clearInterval(bootstrap);
      init();
    }
  }, 500);

})();