humble-bundle-extra

User script for humble bundle. Adds steam store links to all games and marks already owned games

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         humble-bundle-extra
// @namespace    https://humblebundle.com
// @version      1.6.0
// @description  User script for humble bundle. Adds steam store links to all games and marks already owned games
// @match        *://*.humblebundle.com/*
// @author       MrMarble
// @grant        GM_xmlhttpRequest
// @connect      api.steampowered.com
// @connect      store.steampowered.com
// @icon         https://humblebundle-a.akamaihd.net/static/hashed/47e474eed38083df699b7dfd8d29d575e3398f1e.ico
// @license      MIT
// @source       https://github.com/MrMarble/humble-bundle-extra
// ==/UserScript==
(function () {
  'use strict';

  const xtmlHttp = (options) => {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        timeout: 3000,
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        ...options,
        onload: resolve,
        onabort: reject,
        ontimeout: reject,
        onerror: reject,
      });
    })
  };
  const decodeEntities = (() => {
    const element = document.createElement("div");
    function decodeHTMLEntities(str) {
      if (str && typeof str === "string") {
        str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gim, "");
        str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gim, "");
        element.innerHTML = str;
        str = element.textContent;
        element.textContent = "";
      }
      return str
    }
    return decodeHTMLEntities
  })();
  const sanitize = (str) => {
    return decodeEntities(str)
      .replace(/[\u{2122}\u{00AE}\n]/gu, "")
      .trim()
      .toLowerCase()
  };
  const htmlToElement = (html) => {
    var template = document.createElement("template");
    html = html.trim();
    template.innerHTML = html;
    return template.content.firstChild
  };
  const isBundlePage = () => {
    return !!document.querySelector("div.inner-main-wrapper div.bundle-page")
  };
  const isChoicePage = () => {
    return !!document.querySelector(
      `div.inner-main-wrapper div.subscriber-hub,
    div.inner-main-wrapper .js-content-choices`
    )
  };
  const shouldUpdateCache = () => {
    const WEEK = 7 * 24 * 60 * 60 * 1000;
    const lastCached = localStorage.getItem("&&hh_cache&&");
    if (lastCached === null) {
      localStorage.setItem("&&hh_cache&&", Date.now());
      return true
    }
    if (Date.now() - lastCached > WEEK) {
      localStorage.setItem("&&hh_cache&&", Date.now());
      return true
    }
    return false
  };
  const closeModal =
    "(()=> document.querySelector('.charity-details-view.humblemodal-wrapper').remove())()";
  const createModal = (icon, title, text) =>
    htmlToElement(`
  <div class="charity-details-view humblemodal-wrapper" tabindex="0">
    <div class="humblemodal-modal humblemodal-modal--open" style="opacity: 1;">
      <span class="js-close-modal close-modal" onclick="${closeModal}">
        <i class="hb hb-times"></i>
      </span>
      <div class="charity-info-wrapper">
        <div class="charity-media">
          <div class="charity-logo">
            <i class="hb ${icon}" style="font-size:13em;color:#c9262c"></i>
          </div> 
        </div>
        <div class="charity-details">
          <div class="charity-title">
            <h2>${title}</h2>
          </div>
          <div class="charity-description">
            ${text}
          </div>
        </div>
        </div>
    </div>
</div>
  `);

  const CACHE_STEAM_APPS_KEY = "&&hh_extras&&";
  const CACHE_OWNED_APPS_KEY = "&&hh_extras_owned&&";
  const fetchSteamApps = async () => {
    const apps = {};
    try {
      const r = await xtmlHttp({
        url: "https://api.steampowered.com/ISteamApps/GetAppList/v0002/?format=json",
        method: "GET",
        timeout: 5000,
      });
      const { applist } = JSON.parse(r.responseText);
      applist?.apps?.forEach(({ name, appid }) => {
        apps[sanitize(name)] = appid;
      });
    } catch (error) {
      console.error(error);
    }
    return apps
  };
  const cacheSteamApps = async (force) => {
    let data = {};
    try {
      if (force) {
        data = await fetchSteamApps();
        localStorage.setItem(CACHE_STEAM_APPS_KEY, JSON.stringify(data));
      } else {
        if ((data = localStorage.getItem(CACHE_STEAM_APPS_KEY))) {
          data = JSON.parse(data);
        } else {
          data = await fetchSteamApps();
          localStorage.setItem(CACHE_STEAM_APPS_KEY, JSON.stringify(data));
        }
      }
    } catch (error) {
      console.error(error);
    }
    return data
  };
  const fetchOwnedApps = async () => {
    const r = await xtmlHttp({
      url: `https://store.steampowered.com/dynamicstore/userdata/?boost=${Date.now()}`,
      method: "GET",
    });
    const { rgOwnedApps } = JSON.parse(r.responseText);
    return rgOwnedApps
  };
  const cacheOwnedApps = async (force) => {
    let data = [];
    if (force) {
      data = await fetchOwnedApps();
      localStorage.setItem(CACHE_OWNED_APPS_KEY, JSON.stringify(data));
    } else {
      if ((data = localStorage.getItem(CACHE_OWNED_APPS_KEY))) {
        data = JSON.parse(data);
      } else {
        data = await fetchOwnedApps();
        localStorage.setItem(CACHE_OWNED_APPS_KEY, JSON.stringify(data));
      }
    }
    return data
  };
  const clearOwnedCache = () =>
    localStorage.removeItem(CACHE_OWNED_APPS_KEY);

  const HIDE_MODAL = "&&hh_extras_modal&&";
  function showModal() {
    const modal = createModal(
      "hb-exclamation-circle",
      "You are not logged in to the steam store or your profile is private",
      `<p>Information about games already in your library will not be available.</p>
    <p>You can login using this <a href="https://store.steampowered.com/login" target="_blank" rel="noopener">link</a>. Reload the page after login to load the games in your library.</p>
    <p><div class="cta-button rectangular-button button-v2 red js-hero-cta" onclick="(function(){localStorage.setItem('${HIDE_MODAL}',1)})();${closeModal}">Don't show again</div></p>`
    );
    document.querySelector("#site-modal").appendChild(modal);
  }
  async function bundle() {
    const apps = await cacheSteamApps();
    const owned = await cacheOwnedApps();
    const loggedIn = owned.length != 0;
    if (!loggedIn) {
      clearOwnedCache();
      if (!localStorage.getItem(HIDE_MODAL)) {
        showModal();
      }
    }
    document.querySelectorAll(".tier-item-view .item-title").forEach((el) => {
      let appid;
      if ((appid = apps[sanitize(el.textContent)])) {
        const url = `https://store.steampowered.com/app/${appid}`;
        el.innerHTML = `<a href="${url}" style="text-decoration:underline;color:#ecf1fe" target="_blank" rel="noopener" title="Visit Steam Store" onclick="(()=> window.open('${url}','_blank'))()">${el.textContent}</a>`;
        if (loggedIn && owned.includes(appid)) {
          el.firstChild.style.color = "#7f9a2f";
          el.parentElement.parentElement.style.opacity = "25%";
          el.parentElement.parentElement.style.order = parseInt(el.parentElement.parentElement.style.order)+100;
        }
      }
    });
  }
  async function choice() {
    const force = shouldUpdateCache();
    const apps = await cacheSteamApps(force);
    const owned = await cacheOwnedApps(force);
    const loggedIn = owned.length != 0;
    if (!loggedIn) {
      clearOwnedCache();
      if (!localStorage.getItem(HIDE_MODAL)) {
        showModal();
      }
    }
    document.querySelectorAll(".content-choice-title").forEach((el) => {
      let appid;
      if ((appid = apps[sanitize(el.textContent)])) {
        el.innerHTML = `<a href="https://store.steampowered.com/app/${appid}" style="text-decoration:underline;color:#ecf1fe" target="_blank" rel="noopener" title="Visit Steam Store">${el.textContent}</a>`;
        if (loggedIn && owned.includes(appid)) {
          el.firstChild.style.color = "#7f9a2f";
          el.parentElement.parentElement.style.opacity = "25%";
          el.parentElement.parentElement.style.order = parseInt(el.parentElement.parentElement.style.order)+100;
        }
      }
    });
  }
  if (isBundlePage()) {
    bundle();
  } else if (isChoicePage()) {
    choice();
  }

})();