Steam explicit discounts

Some games has just "on sale" tag, instead of percentage discount. This script replaces discount icon with percentage and adds calculated full price. Script is not downloading any info, it is using only the info hidden on a page. This is not available everywhere on Steam, so only some pages are supported: wishlist, search, main page, product, DLC, bundle, widget. Yeah, Steam just complies with stupid EU law.

当前为 2024-07-08 提交的版本,查看 最新版本

// ==UserScript==
// @name         Steam explicit discounts
// @version      2024.7.8
// @namespace    Jakub Marcinkowski
// @description  Some games has just "on sale" tag, instead of percentage discount. This script replaces discount icon with percentage and adds calculated full price. Script is not downloading any info, it is using only the info hidden on a page. This is not available everywhere on Steam, so only some pages are supported: wishlist, search, main page, product, DLC, bundle, widget. Yeah, Steam just complies with stupid EU law.
// @author       Jakub Marcinkowski <kuba.marcinkowski on g mail>
// @copyright    2024+, Jakub Marcinkowski <kuba.marcinkowski on g mail>
// @license      Zlib
// @homepageURL  https://gist.github.com/JakubMarcinkowski
// @homepageURL  https://github.com/JakubMarcinkowski
// @run-at       document-idle
// @grant        unsafeWindow
// @icon         https://help.steampowered.com/public/shared/images/responsive/share_steam_logo.png
// @match        *://store.steampowered.com/*
// @exclude      *://store.steampowered.com/specials/*
// @exclude      *://store.steampowered.com/genre/*
// @exclude      *://store.steampowered.com/category/*
// @exclude      *://store.steampowered.com/explore/*
// @exclude      *://store.steampowered.com/greatondeck*
// @exclude      *://store.steampowered.com/controller*
// @exclude      *://store.steampowered.com/remoteplay_hub*
// @exclude      *://store.steampowered.com/vr*
// @exclude      *://store.steampowered.com/vrhardware*
// @exclude      *://store.steampowered.com/software*
// @exclude      *://store.steampowered.com/soundtracks*
// @exclude      *://store.steampowered.com/macos*
// @exclude      *://store.steampowered.com/linux*
// @exclude      *://store.steampowered.com/pccafe*
// @exclude      *://store.steampowered.com/adultonly*
// @exclude      *://store.steampowered.com/sale/nextfest*
// @exclude      *://store.steampowered.com/sale/steam_awards*
// @exclude      *://store.steampowered.com/steamawards*
// @exclude      *://store.steampowered.com/steamdeck*
// @exclude      *://store.steampowered.com/points*
// @exclude      *://store.steampowered.com/news*
// @exclude      *://store.steampowered.com/yearinreview*
// @exclude      *://store.steampowered.com/labs*
// @exclude      *://store.steampowered.com/charts*
// @exclude      *://store.steampowered.com/about*
// ==/UserScript==

(function() {
  'use strict';

  const priceExample = document.getElementsByClassName('discount_final_price')[0];
  if (!priceExample) return;
  const priceChunks = [...priceExample.textContent.matchAll(/(.*?)([0-9]+[,.]*[0-9]+)(.*)/g)][0];

  if (location.pathname === '/'
      || location.pathname.startsWith('/dlc/')
     ) {
    const observer = new MutationObserver(function(mutationsList, observer2) {
      for (const mutation of mutationsList) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== Node.ELEMENT_NODE
              || node.className.startsWith('discount')
              || [...node.classList].find(x => ['ds_options', 'ds_wishlist_flag'].includes(x))
              || node.tagName === 'VIDEO'
             ) continue;
          convertAllIn(node);
        }
      }
    });
    const ids = ['content_more', 'RecommendationsRows'];
    for (const id of ids) {
      const node = document.getElementById(id);
      if (node) observer.observe(node, {childList: true});
    }
    const mainContent = document.getElementsByClassName('main_content_ctn')[0];
    if (mainContent) observer.observe(mainContent, {childList: true, subtree: true});
  }

  if (location.pathname === '/'
      || location.pathname.startsWith('/bundle/')
      || location.pathname.startsWith('/sub/')
      || location.pathname.startsWith('/dlc/')
      || location.pathname.startsWith('/widget/') // embedded iframe strip with basic game info
     ) {
    convertAllIn(document);
  }

  if (location.pathname.startsWith('/app/')) {
    const prices = document.getElementsByClassName('game_purchase_action');
    for (const element of prices) {
      convertAllIn(element);
    }

    const dlcs = document.getElementById('gameAreaDLCSection');
    if (dlcs) {
      convertAllIn(dlcs);
    }

    const ids = ['recommended_block_content', 'franchise_app_block_content', 'moredlcfrombasegame_block_content'];
    const recSelector = ids.map(id => `#${id} .generic_discount`).join(', ');
    const nodes = document.querySelectorAll(recSelector);
    if (nodes.length !== 0) {
      convertAllIn(nodes);
    } else {
      const observer = new MutationObserver(function(mutationsList, observer2) {
        const nodes = document.querySelectorAll(recSelector);
        convertAllIn(nodes);
        observer.disconnect();
      });
      for (const id of ids) {
        const node = document.getElementById(id);
        if (node) observer.observe(node, {childList: true});
      }
    }
  }

  if (location.pathname.startsWith('/wishlist/')) {
    const observer = new MutationObserver(function(mutationsList, observer2) {
      for (const mutation of mutationsList) {
        if (mutation.addedNodes.length === 0) continue;
        convertAllIn(mutation.addedNodes[0]);
      };
    });
    observer.observe(document.getElementById('wishlist_ctn'), {childList: true});
  }

  if (location.pathname.startsWith('/search/')) {
    [...document.getElementById('search_resultsRows').children].forEach(convertAllIn);

    const observer = new MutationObserver(function(mutationsList, observer2) {
      for (const mutation of mutationsList) {
        if (mutation.addedNodes.length !== 1
            || !(mutation.addedNodes[0].tagName && mutation.addedNodes[0].tagName === 'A')
           ) continue;
        convertAllIn(mutation.addedNodes[0]);
      };
    });
    observer.observe(document.getElementById('search_resultsRows'), {childList: true});
  }

  function convertAllIn(node) {
    for (const elem of node.querySelectorAll('.generic_discount')) { // getElementsByClassName is troublesome, probably as it gives live list
      convertPrices(elem);
    }
  }

  function convertPrices(generic) {
    const discount_block = generic.parentElement; // div.discount_block

    const discount_prices = discount_block.getElementsByClassName('discount_prices')[0];
    const discount_icon = discount_block.getElementsByClassName('discount_icon')[0];

    discount_icon.style.display = 'none';
    generic.classList.remove('generic_discount');

    const discount_pct = document.createElement('div');
    discount_pct.className = 'discount_pct';
    discount_block.prepend(discount_pct);

    const discount_original_price = document.createElement('div');
    discount_original_price.className = 'discount_original_price';
    discount_prices.prepend(discount_original_price);

    const discount = discount_block.dataset.discount;
    const discountText = (-discount / 100).toLocaleString(navigator.language,{ style: 'percent'});
    const priceFinal = discount_block.dataset.priceFinal;
    const priceCalc = priceFinal / (100 - discount);

    let priceText = '';
    // try {
    //   priceText = unsafeWindow.GStoreItemData.fnFormatCurrency(priceCalc * 100);
    // } catch {
      priceText = Number(priceCalc.toFixed(2)).toLocaleString();
      if (priceChunks && priceChunks[1]) priceText = priceChunks[1] + priceText;
      else if (priceChunks && priceChunks[3]) priceText = priceText + priceChunks[3];
    // }

    discount_pct.textContent = discountText;
    discount_original_price.textContent = priceText;
console.log(priceText);
  }
})();