GG.deals Steam Companion

Shows lowest price from gg.deals on Steam game pages

// ==UserScript==
// @name         GG.deals Steam Companion
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Shows lowest price from gg.deals on Steam game pages
// @author       Crimsab
// @license      GPL-3.0-or-later
// @match        https://store.steampowered.com/app/*
// @match        https://store.steampowered.com/sub/*
// @match        https://store.steampowered.com/bundle/*
// @icon         https://gg.deals/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        unsafeWindow
// @connect      gg.deals
// @connect      api.gg.deals
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

// KNOWN LIMITATIONS: 
// Bundles always use web scraping, never API. The official api does not support steam bundles, giving null results.
// Subs (packages) and Apps can use either API or web scraping.
// "Enable Scraping" toggle controls whether web scraping is used when API is disabled or fails.

(function () {
  "use strict";

  // Default color scheme
  const defaultColors = {
    background: "#16202d",
    headerBackground: "#0d141c",
    officialText: "#67c1f5",
    officialPrice: "#ffffff",
    keyshopText: "#67c1f5",
    keyshopPrice: "#ffffff",
    bestPrice: "#a4d007",
    buttonBackground: "linear-gradient(to right, #67c1f5 0%, #4a9bd5 100%)",
    buttonText: "#ffffff",
    borderColor: "#67c1f530"
  };

  // Get saved colors or use defaults
  const savedColors = {};
  Object.keys(defaultColors).forEach(key => {
    savedColors[key] = GM_getValue(`color_${key}`, defaultColors[key]);
  });

  // Function to apply colors
  function applyCustomColors() {
    let customCSS = `
      :root {
        --gg-deals-background: ${savedColors.background};
        --gg-deals-header-bg: ${savedColors.headerBackground};
        --gg-deals-official-text: ${savedColors.officialText};
        --gg-deals-official-price: ${savedColors.officialPrice};
        --gg-deals-keyshop-text: ${savedColors.keyshopText};
        --gg-deals-keyshop-price: ${savedColors.keyshopPrice};
        --gg-deals-best-price: ${savedColors.bestPrice};
        --gg-deals-button-bg: ${savedColors.buttonBackground};
        --gg-deals-button-text: ${savedColors.buttonText};
        --gg-deals-border-color: ${savedColors.borderColor};
      }
    `;
    
    // Create a style element for our custom colors
    const styleEl = document.getElementById('gg-deals-custom-colors') || document.createElement('style');
    styleEl.id = 'gg-deals-custom-colors';
    styleEl.textContent = customCSS;
    document.head.appendChild(styleEl);
  }

  GM_addStyle(`
        .gg-deals-container {
            background: var(--gg-deals-background, #16202d) !important;
            border-radius: 4px;
            padding: 15px;
            margin: 20px 0;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            width: 100%;
            max-width: 100%;
            box-sizing: border-box;
            clear: both;
        }
        .gg-deals-container.compact {
            padding: 10px;
            margin: 10px 0;
        }
        .gg-deals-container.compact .gg-header,
        .gg-deals-container.compact .gg-attribution,
        .gg-deals-container.compact .gg-price-sections {
            display: none;
        }
        .gg-compact-row {
            display: none;
            align-items: center;
            gap: 15px;
            padding: 5px;
            flex-wrap: nowrap;
            min-width: 0;
        }
        .gg-deals-container.compact .gg-compact-row {
            display: flex;
        }
        .gg-compact-prices {
            display: flex;
            align-items: center;
            gap: 20px;
            flex: 1;
            min-width: 0;
            overflow: visible;
        }
        .gg-compact-price-item {
            display: flex;
            align-items: center;
            position: relative;
            gap: 8px;
            min-width: 0;
            flex-shrink: 1;
        }
        .gg-compact-price-item .gg-price-value {
            font-size: 18px;
        }
        .gg-price-value.best-price {
            color: var(--gg-deals-best-price, #a4d007);
            position: relative;
            padding-top: 16px;
        }
        .gg-price-value.best-price:before {
            content: "✓ Best Price";
            position: absolute;
            right: 0;
            top: 0;
            font-size: 12px;
            opacity: 0.9;
            color: var(--gg-deals-best-price, #a4d007);
            white-space: nowrap;
        }
        /* Hide the "Best Price" text in compact view */
        .gg-compact-price-item .gg-price-value.best-price {
            padding-top: 0;
        }
        .gg-compact-price-item .gg-price-value.best-price:before {
            display: none;
        }
        .gg-settings-dropdown {
            position: relative;
            display: inline-block;
        }
        .gg-settings-icon {
            cursor: pointer;
            padding: 5px;
            opacity: 0.7;
            transition: opacity 0.2s;
        }
        .gg-settings-icon:hover {
            opacity: 1;
        }
        .gg-settings-icon svg {
            width: 20px;
            height: 20px;
            fill: var(--gg-deals-official-text, #67c1f5);
        }
        .gg-settings-content {
            display: none;
            position: absolute;
            right: 0;
            background: var(--gg-deals-background, #16202d);
            min-width: 160px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            border-radius: 4px;
            z-index: 1000;
            padding: 10px;
        }
        .gg-settings-content.show {
            display: block;
        }
        .gg-compact-controls {
            display: flex;
            align-items: center;
            gap: 10px;
            flex-shrink: 0;
        }
        .gg-tooltip {
            position: relative;
            display: inline-block;
        }
        .gg-tooltip:hover .gg-tooltip-text {
            visibility: visible;
            opacity: 1;
        }
        .gg-tooltip-text {
            visibility: hidden;
            opacity: 0;
            background-color: var(--gg-deals-background, #16202d);
            color: #fff;
            text-align: center;
            padding: 5px 10px;
            border-radius: 4px;
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            position: absolute;
            z-index: 1;
            bottom: 125%;
            left: 50%;
            transform: translateX(-50%);
            white-space: nowrap;
            transition: opacity 0.2s;
            font-size: 12px;
        }
        /* New historical tooltip styles */
        .gg-historical-tooltip {
            position: relative;
            display: inline-block;
        }
        .gg-historical-tooltip:hover .gg-historical-tooltip-text {
            visibility: visible;
            opacity: 1;
        }
        .gg-historical-tooltip-text {
            visibility: hidden;
            opacity: 0;
            background-color: var(--gg-deals-background, #16202d);
            color: #fff;
            text-align: center;
            padding: 5px 10px;
            border-radius: 4px;
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            position: absolute;
            z-index: 1;
            bottom: 125%;
            left: 50%;
            transform: translateX(-50%);
            white-space: nowrap;
            transition: opacity 0.2s;
            font-size: 12px;
        }
        .gg-controls {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid var(--gg-deals-border-color, rgba(103, 193, 245, 0.1));
        }
        .gg-header {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: -15px -15px 15px -15px;
            padding: 15px;
            background: var(--gg-deals-header-bg, rgb(13, 20, 28));
            border-radius: 4px 4px 0 0;
            border-bottom: 1px solid var(--gg-deals-border-color, rgba(103, 193, 245, 0.2));
            text-align: center;
        }
        .gg-title {
            color: var(--gg-deals-official-text, #67c1f5);
            font-size: 24px;
            font-weight: bold;
            text-transform: uppercase;
            letter-spacing: 1px;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
            display: flex;
            align-items: center;
            gap: 12px;
        }
        .gg-title img {
            width: 32px;
            height: 32px;
            filter: brightness(1.2) drop-shadow(1px 1px 2px rgba(0,0,0,0.5));
        }
        .gg-attribution {
            color: #8f98a0;
            font-size: 11px;
            opacity: 0.8;
            font-style: italic;
            text-align: center;
            margin-top: 12px;
            padding-top: 12px;
            border-top: 1px solid var(--gg-deals-border-color, rgba(103, 193, 245, 0.1));
        }
        .gg-price-sections {
            display: flex;
            justify-content: space-between;
            margin: 8px 0;
            padding: 12px;
            background: #1b2838;
            border-radius: 3px;
            transition: all 0.3s ease;
            position: relative;
            min-height: 60px;
        }
        .gg-price-section {
            display: flex;
            flex-direction: column;
            gap: 4px;
            flex: 1;
            min-width: 0;
        }
        .gg-price-left {
            display: flex;
            align-items: center;
            justify-content: center;
            flex: 1;
        }
        .gg-price-label {
            color: var(--gg-deals-official-text, #67c1f5);
            font-size: 14px;
            display: flex;
            align-items: center;
            gap: 8px;
            white-space: nowrap;
        }
        .gg-price-info {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-width: 120px;
            text-align: center;
            margin-left: 20px;
        }
        .gg-price-value {
            color: var(--gg-deals-official-price, #fff);
            font-weight: bold;
            font-size: 24px;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
            transition: color 0.3s ease;
            white-space: nowrap;
        }
        .gg-price-value.historical {
            font-size: 13px;
            color: var(--gg-deals-official-text, #acdbf5);
            opacity: 0.9;
            margin-top: 4px;
        }
        .gg-icon {
            width: 20px;
            height: 20px;
            filter: brightness(0.8);
            flex-shrink: 0;
        }
        .gg-footer {
            margin-top: 12px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }
        .gg-view-offers {
            width: 100%;
            background: var(--gg-deals-button-bg, linear-gradient(to right, #67c1f5 0%, #4a9bd5 100%));
            padding: 8px 20px;
            border-radius: 3px;
            color: var(--gg-deals-button-text, #fff) !important;
            font-size: 14px;
            text-decoration: none !important;
            transition: all 0.2s ease;
            text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
            text-align: center;
            white-space: nowrap;
        }
        .gg-view-offers:hover {
            background: var(--gg-deals-button-bg, linear-gradient(to right, #7dcbff 0%, #4a9bd5 100%));
            transform: translateY(-1px);
        }
        .gg-toggles {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        .gg-toggle {
            display: flex;
            align-items: center;
            gap: 5px;
            cursor: pointer;
            user-select: none;
            opacity: 0.7;
            transition: opacity 0.2s ease;
            white-space: nowrap;
            color: var(--gg-deals-official-text, #67c1f5);
        }
        .gg-toggle:hover {
            opacity: 1;
        }
        .gg-toggle.active {
            opacity: 1;
        }
        .gg-toggle input {
            margin: 0;
        }
        .gg-toggle label {
            color: var(--gg-deals-official-text, #67c1f5);
            font-size: 12px;
        }

        @media (max-width: 640px) {
            .gg-price-sections {
                flex-direction: column;
                align-items: stretch;
                gap: 10px;
            }
            .gg-price-info {
                align-items: flex-start;
                margin-left: 28px;
            }
            .gg-price-value.best-price {
                padding-top: 0;
                padding-right: 80px;
            }
            .gg-price-value.best-price:before {
                top: 50%;
                transform: translateY(-50%);
                right: 0;
            }
            .gg-footer {
                flex-direction: column-reverse;
                align-items: stretch;
            }
            .gg-view-offers {
                text-align: center;
            }
            .gg-toggles {
                justify-content: center;
            }
        }

        .gg-icon-button {
            background: none;
            border: none;
            color: var(--gg-deals-official-text, #67c1f5);
            cursor: pointer;
            padding: 5px;
            border-radius: 3px;
            display: flex;
            align-items: center;
            gap: 5px;
            font-size: 12px;
            opacity: 0.7;
            transition: all 0.2s ease;
        }
        .gg-icon-button:hover {
            opacity: 1;
            background: rgba(103, 193, 245, 0.1);
        }
        .gg-icon-button svg {
            width: 20px;
            height: 20px;
            fill: currentColor;
        }
        .gg-refresh {
            padding: 5px 8px;
            display: flex;
            align-items: center;
            min-width: max-content;
            flex-shrink: 0;
            position: relative;
        }
        .gg-refresh svg {
            transition: transform 0.5s ease;
            stroke: currentColor;
            fill: none;
        }
        .gg-refresh.loading svg {
            transform: rotate(360deg);
        }
        .gg-refresh-text {
            display: none;
        }
        .gg-refresh:hover .gg-tooltip-text {
            visibility: visible;
            opacity: 1;
        }

        .github-icon {
            width: 16px;
            height: 16px;
            vertical-align: middle;
            margin: -2px 4px 0 2px;
            opacity: 0.8;
            transition: opacity 0.2s ease;
        }
        .github-icon:hover {
            opacity: 1;
        }
        .gg-deals-container.compact .gg-controls {
            display: none;
        }
        .bundle-sub-display {
            background: var(--gg-deals-background, #16202d) !important;
            border-radius: 4px;
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            position: relative;
            z-index: 1;
        }
        .game_area_purchase_game + .bundle-sub-display {
            margin-top: -10px !important;
        }
        .bundle_contents_preview + .gg-deals-container {
            margin-top: 0 !important;
        }
        .game_area_purchase + .gg-deals-container {
            margin-top: 0 !important;
        }
        .gg-view-offers {
            display: inline-block;
            text-align: center;
            transition: transform 0.2s ease;
        }
        .gg-view-offers:hover {
            transform: translateY(-1px);
        }
        .gg-price-value {
            display: inline-block;
            min-width: 80px;
        }
        .gg-deals-container.compact .gg-view-offers {
            width: auto;
            min-width: 90px;
            white-space: nowrap;
            flex-shrink: 0;
        }
        .gg-api-key-input {
            width: 100%;
            padding: 6px 8px;
            margin: 8px 0;
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            border-radius: 3px;
            background: #121b28;
            color: #fff;
            font-size: 12px;
            box-sizing: border-box;
        }
        .gg-region-select {
            width: 100%;
            padding: 6px 8px;
            margin: 8px 0;
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            border-radius: 3px;
            background: #121b28;
            color: #fff;
            font-size: 12px;
            box-sizing: border-box;
        }
        .gg-settings-section {
            margin-bottom: 10px;
            padding-bottom: 10px;
            border-bottom: 1px solid var(--gg-deals-border-color, rgba(103, 193, 245, 0.1));
        }
        .gg-settings-title {
            color: var(--gg-deals-official-text, #67c1f5);
            font-size: 13px;
            margin-bottom: 5px;
        }
        .gg-settings-content {
            width: 270px !important;
            max-width: 270px !important;
        }
        .gg-api-status {
            font-size: 11px;
            margin-top: 4px;
        }
        .gg-api-status.active {
            color: var(--gg-deals-best-price, #a4d007);
        }
        .gg-api-status.inactive {
            color: #ff7b7b;
        }
        .gg-save-button {
            background: var(--gg-deals-button-bg, linear-gradient(to right, #67c1f5 0%, #4a9bd5 100%));
            border: none;
            padding: 5px 10px;
            border-radius: 3px;
            color: var(--gg-deals-button-text, #fff);
            font-size: 12px;
            cursor: pointer;
            margin-top: 5px;
            transition: all 0.2s ease;
            width: 100%;
        }
        .gg-save-button:hover {
            background: var(--gg-deals-button-bg, linear-gradient(to right, #7dcbff 0%, #4a9bd5 100%));
        }
        .gg-api-key-wrapper {
            position: relative;
            width: 100%;
        }
        .gg-toggle-visibility {
            position: absolute;
            right: 8px;
            top: 50%;
            transform: translateY(-50%);
            background: none;
            border: none;
            color: var(--gg-deals-official-text, #67c1f5);
            cursor: pointer;
            opacity: 0.7;
            transition: opacity 0.2s;
            padding: 0;
            width: 18px;
            height: 18px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .gg-toggle-visibility:hover {
            opacity: 1;
        }
        .gg-toggle-visibility svg {
            width: 18px;
            height: 18px;
            fill: currentColor;
        }
        
        /* Color picker styles */
        .gg-color-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 8px;
            margin: 10px 0;
        }
        .gg-color-item {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }
        .gg-color-label {
            font-size: 11px;
            color: var(--gg-deals-official-text, #67c1f5);
        }
        .gg-color-input {
            width: 100%;
            height: 24px;
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            border-radius: 3px;
            background: none;
            cursor: pointer;
        }
        .gg-reset-colors {
            background: none;
            border: 1px solid var(--gg-deals-border-color, #67c1f530);
            padding: 5px 10px;
            border-radius: 3px;
            color: var(--gg-deals-official-text, #67c1f5);
            font-size: 12px;
            cursor: pointer;
            margin-top: 5px;
            transition: all 0.2s ease;
            width: 100%;
        }
        .gg-reset-colors:hover {
            background: rgba(103, 193, 245, 0.1);
        }
    /* New styles for full view controls layout */
    .gg-deals-container:not(.compact) .gg-controls {
        display: flex;
        flex-direction: column; /* Main sections stacked */
        gap: 20px; /* Increased gap for better separation */
        margin-top: 20px; /* Increased margin */
        padding-top: 15px;
        border-top: 1px solid var(--gg-deals-border-color, rgba(103, 193, 245, 0.1));
        align-items: stretch; /* Allow children to define their own width fully */
    }

    .gg-deals-container:not(.compact) .gg-main-actions {
        display: flex;
        gap: 15px;
        align-items: center;
    }

    .gg-deals-container:not(.compact) .gg-main-actions .gg-view-offers {
        flex-grow: 1; /* Takes available space */
        width: auto; /* Override general width: 100% for flex context */
    }
    
    .gg-deals-container:not(.compact) .gg-main-actions .gg-refresh {
        flex-shrink: 0; /* Prevent refresh icon from shrinking */
    }

    .gg-deals-container:not(.compact) .gg-settings-panels {
        display: flex;
        flex-direction: row;
        gap: 15px;
        flex-wrap: wrap; /* Allow panels to wrap to next line if not enough space */
    }

    .gg-deals-container:not(.compact) .gg-settings-panels .gg-settings-section {
        flex: 1 1 240px; /* Grow, Shrink, Basis */
        padding: 15px;
        border: 1px solid var(--gg-deals-border-color);
        border-radius: 4px;
        background: rgba(0,0,0,0.05); /* Slightly lighter background for panels */
        margin-bottom: 0; 
        /* border-bottom property from general .gg-settings-section will be overridden by the border property here */
    }
    
    .gg-deals-container:not(.compact) .gg-settings-panels .gg-settings-title { /* More specific selector for title */
        margin-bottom: 12px;
        font-weight: bold;
        color: var(--gg-deals-official-text); 
    }
    /* End of new styles */
    `);

  // Get saved toggle states or set defaults
  const toggleStates = {
    official: GM_getValue("showOfficial", true),
    keyshop: GM_getValue("showKeyshop", true),
    compact: GM_getValue("compactView", false),
    subDisplay: GM_getValue("showSubDisplay", true),
    useApi: GM_getValue("useApi", false),
    enableScraping: GM_getValue("enableScraping", true)
  };
  
  // Force set enableScraping in GM storage if it doesn't exist yet
  if (GM_getValue("enableScraping") === undefined) {
    GM_setValue("enableScraping", true);
    console.log("GG.deals: Initializing enableScraping setting to true");
  }
  
  // Get API key if saved
  const apiKey = GM_getValue("apiKey", "");
  
  // Get preferred region/currency (default: us)
  const preferredRegion = GM_getValue("preferredRegion", "us");
  
  // Available regions for API
  const availableRegions = [
    { code: "us", name: "USA (USD)" },
    { code: "gb", name: "UK (GBP)" },
    { code: "eu", name: "Europe (EUR)" },
    { code: "ca", name: "Canada (CAD)" },
    { code: "au", name: "Australia (AUD)" },
    { code: "br", name: "Brazil (BRL)" },
    { code: "ru", name: "Russia (RUB)" },
    { code: "tr", name: "Turkey (TRY)" },
    { code: "pl", name: "Poland (PLN)" },
    { code: "de", name: "Germany (EUR)" },
    { code: "fr", name: "France (EUR)" },
    { code: "es", name: "Spain (EUR)" },
    { code: "it", name: "Italy (EUR)" },
    { code: "ch", name: "Switzerland (CHF)" },
    { code: "nl", name: "Netherlands (EUR)" },
    { code: "se", name: "Sweden (SEK)" },
    { code: "no", name: "Norway (NOK)" },
    { code: "dk", name: "Denmark (DKK)" },
    { code: "fi", name: "Finland (EUR)" },
    { code: "ie", name: "Ireland (EUR)" },
    { code: "be", name: "Belgium (EUR)" }
  ];

  // Apply custom colors on script load
  applyCustomColors();

  // Cache configuration
  const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours
  const RATE_LIMIT_DELAY = 2000; // 2 seconds between requests
  const MAX_RETRIES = 1;
  
  // Global variables for easy access
  window.ggDealsApiKey = apiKey;
  window.ggDealsRegion = preferredRegion;

  // In-memory cache to reduce GM_getValue calls
  const memoryCache = {};

  // Cache structure with force refresh option and memory caching
  const priceCache = {
    get: function (key, forceRefresh = false) {
      if (forceRefresh) {
        // Clear both memory and persistent cache
        delete memoryCache[key];
        GM_setValue(`cache_${key}`, "");
        return null;
      }

      // Check memory cache first for better performance
      if (memoryCache[key]) {
        const { data, timestamp } = memoryCache[key];
        if (Date.now() - timestamp <= CACHE_EXPIRY) {
          return data;
        }
        // Expired cache, remove from memory
        delete memoryCache[key];
      }

      // Otherwise check persistent storage
      const cached = GM_getValue(`cache_${key}`);
      if (!cached) return null;

      try {
        const cacheObject = JSON.parse(cached);
        const { data, timestamp, source } = cacheObject;
        
        if (Date.now() - timestamp > CACHE_EXPIRY) {
          // Expired cache, clear it
          GM_setValue(`cache_${key}`, "");
          return null;
        }
        
        // Store in memory cache for future access
        memoryCache[key] = { data, timestamp, source };
        return data;
      } catch (e) {
        // Invalid cache data
        console.warn(`GG.deals: Invalid cache data for ${key}`, e);
        GM_setValue(`cache_${key}`, "");
        return null;
      }
    },
    set: function (key, data, source = "web") {
      if (!data) return; // Don't cache null/undefined data
      
      const cacheObject = {
        data: data,
        timestamp: Date.now(),
        source: source
      };
      
      // Update memory cache
      memoryCache[key] = cacheObject;
      
      // Update persistent storage
      GM_setValue(`cache_${key}`, JSON.stringify(cacheObject));
      
      // Periodically clean old cache entries
      this.cleanExpiredEntries();
    },
    getTimestamp: function (key) {
      // Check memory cache first
      if (memoryCache[key]) {
        return memoryCache[key].timestamp;
      }
      
      const cached = GM_getValue(`cache_${key}`);
      if (!cached) return null;
      
      try {
        return JSON.parse(cached).timestamp;
      } catch (e) {
        return null;
      }
    },
    getSource: function (key) {
      // Check memory cache first
      if (memoryCache[key]) {
        return memoryCache[key].source;
      }
      
      const cached = GM_getValue(`cache_${key}`);
      if (!cached) return null;
      
      try {
        return JSON.parse(cached).source;
      } catch (e) {
        return null;
      }
    },
    // Method to clean expired entries (runs occasionally)
    cleanExpiredEntries: function() {
      // Only run cleanup occasionally (1 in 10 chance)
      if (Math.random() < 0.1) {
        const now = Date.now();
        
        // Clean memory cache
        Object.keys(memoryCache).forEach(key => {
          if (now - memoryCache[key].timestamp > CACHE_EXPIRY) {
            delete memoryCache[key];
          }
        });
        
        // We could do this for GM storage but it's expensive to enumerate all keys
        // Let individual expired entries be cleared on access instead
      }
    }
  };

  // Rate limiter with memory-based tracking and cross-tab synchronization
  const requestTracker = {
    lastRequestTime: 0,
    activeRequests: {}
  };
  
  async function rateLimitedRequest(url) {
    const now = Date.now();
    const urlHash = url.split('?')[0]; // Base URL without params for tracking
    
    // Check if we have an active request for this URL
    if (requestTracker.activeRequests[urlHash]) {
      const activeRequest = requestTracker.activeRequests[urlHash];
      try {
        // Reuse the existing request
        console.log(`GG.deals: Reusing in-flight request for ${urlHash}`);
        return await activeRequest;
      } catch (error) {
        // If the existing request failed, continue with a new one
        console.warn(`GG.deals: Reused request failed, trying again: ${error}`);
      }
    }
    
    // Get last request time from global storage (shared between tabs)
    const storedLastRequest = GM_getValue("lastRequestTime", 0);
    requestTracker.lastRequestTime = Math.max(requestTracker.lastRequestTime, storedLastRequest);
    
    const timeToWait = Math.max(0, RATE_LIMIT_DELAY - (now - requestTracker.lastRequestTime));

    if (timeToWait > 0) {
      await new Promise((resolve) => setTimeout(resolve, timeToWait));
    }

    // Update both local tracker and global storage
    requestTracker.lastRequestTime = Date.now();
    GM_setValue("lastRequestTime", requestTracker.lastRequestTime);

    // Create a new request
    const requestPromise = new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: url,
        timeout: 10000,
        onload: (response) => {
          // Clear the tracked request when done
          delete requestTracker.activeRequests[urlHash];
          resolve(response);
        },
        onerror: (error) => {
          // Clear the tracked request when failed
          delete requestTracker.activeRequests[urlHash];
          reject(error);
        },
        ontimeout: (error) => {
          // Clear the tracked request when timed out
          delete requestTracker.activeRequests[urlHash];
          reject(error);
        }
      });
    });
    
    // Track this request
    requestTracker.activeRequests[urlHash] = requestPromise;
    
    return requestPromise;
  }

  // New function to fetch data using GG.deals API
  async function fetchGamePricesApi(steamId, steamType) {
    // Check for valid API key
    const apiKey = GM_getValue("apiKey", "");
    if (!apiKey) {
      throw new Error("No API key provided");
    }
    
    // Get preferred region
    const region = GM_getValue("preferredRegion", "us");

    // Set up the API URL based on the steam type
    let apiUrl;
    if (steamType === 'sub') {
      apiUrl = `https://api.gg.deals/v1/prices/by-steam-sub-id/?ids=${steamId}&key=${apiKey}&region=${region}`;
    } else {
      apiUrl = `https://api.gg.deals/v1/prices/by-steam-app-id/?ids=${steamId}&key=${apiKey}&region=${region}`;
    }
    
    try {
      const response = await rateLimitedRequest(apiUrl);
      
      // Check for successful response
      if (response.status !== 200) {
        throw new Error(`API returned status ${response.status}`);
      }
      
      // Parse the JSON response
      const jsonData = JSON.parse(response.responseText);
      
      // Check for successful API response
      if (!jsonData.success) {
        throw new Error(jsonData.data?.message || "API error");
      }
      
      // Check if data exists for this game
      if (!jsonData.data || !jsonData.data[steamId]) {
        throw new Error("No data found for this game");
      }
      
      const gameData = jsonData.data[steamId];
      
      // Handle case where game is not found in API
      if (gameData === null) {
        throw new Error("Game not found in GG.deals database");
      }
      
      // Get currency symbol based on region
      const currencySymbols = {
        us: "$", eu: "€", gb: "£", ca: "CA$", au: "A$", br: "R$", 
        ru: "₽", tr: "₺", pl: "zł", fr: "€", de: "€", es: "€", 
        it: "€", ch: "CHF", nl: "€", se: "kr", no: "kr", dk: "kr", 
        fi: "€", ie: "€", be: "€"
      };
      const currencySymbol = currencySymbols[region] || gameData.prices.currency || "";
      
      // Format prices with currency symbol
      const formatPrice = (price) => {
        if (!price) return "No data";
        // Check if currency is before or after the number based on region
        if (["us", "ca", "au", "br"].includes(region)) {
          return `${currencySymbol}${price}`;
        } else {
          return `${price}${currencySymbol}`;
        }
      };
      
      // Format the data to match our expected structure
      const formattedData = {
        officialPrice: formatPrice(gameData.prices.currentRetail),
        keyshopPrice: formatPrice(gameData.prices.currentKeyshops),
        historicalData: [],
        lowestPriceType: null,
        url: gameData.url,
        isCorrectGame: true
      };
      
      // Add historical data if available
      if (gameData.prices.historicalRetail) {
        formattedData.historicalData.push({
          type: "official",
          price: gameData.prices.historicalRetail,
          historical: `Historical Low: ${formatPrice(gameData.prices.historicalRetail)}`
        });
      }
      
      if (gameData.prices.historicalKeyshops) {
        formattedData.historicalData.push({
          type: "keyshop",
          price: gameData.prices.historicalKeyshops,
          historical: `Historical Low: ${formatPrice(gameData.prices.historicalKeyshops)}`
        });
      }
      
      // Determine lowest price type
      if (gameData.prices.currentRetail && gameData.prices.currentKeyshops) {
        const officialPriceNum = parseFloat(gameData.prices.currentRetail);
        const keyshopPriceNum = parseFloat(gameData.prices.currentKeyshops);
        formattedData.lowestPriceType = officialPriceNum <= keyshopPriceNum ? "official" : "keyshop";
      } else if (gameData.prices.currentRetail) {
        formattedData.lowestPriceType = "official";
      } else if (gameData.prices.currentKeyshops) {
        formattedData.lowestPriceType = "keyshop";
      }
      
      return formattedData;
    } catch (error) {
      console.error("GG.deals API error:", error);
      throw error;
    }
  }

  function createPriceContainer() {
    const container = document.createElement("div");
    // Get the saved compact state
    const isCompact = GM_getValue("compactView", false);
    container.className = "gg-deals-container" + (isCompact ? " compact" : "");
    container.innerHTML = `
            <div class="gg-header">
                <div class="gg-title">
                    <img src="https://gg.deals/favicon.ico" alt="GG.deals">
                    GG.deals Steam Companion
                </div>
            </div>
            <div class="gg-compact-row">
                <img src="https://gg.deals/favicon.ico" alt="GG.deals" class="gg-icon">
                <div class="gg-compact-prices">
                    <div class="gg-compact-price-item" id="gg-compact-official" style="${
                      !toggleStates.official ? "display:none" : ""
                    }">
                        <span>Official:</span>
                        <span class="gg-historical-tooltip">
                            <span class="gg-price-value" id="gg-compact-official-price">Loading...</span>
                            <span class="gg-historical-tooltip-text" id="gg-compact-official-historical"></span>
                        </span>
                    </div>
                    <div class="gg-compact-price-item" id="gg-compact-keyshop" style="${
                      !toggleStates.keyshop ? "display:none" : ""
                    }">
                        <span>Keyshop:</span>
                        <span class="gg-historical-tooltip">
                            <span class="gg-price-value" id="gg-compact-keyshop-price">Loading...</span>
                            <span class="gg-historical-tooltip-text" id="gg-compact-keyshop-historical"></span>
                        </span>
                    </div>
                </div>
                <div class="gg-compact-controls">
                    <button class="gg-icon-button gg-refresh" title="Refresh Prices">
                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
                        </svg>
                        <span class="gg-tooltip-text">Click to refresh prices</span>
                    </button>
                    <div class="gg-settings-dropdown">
                        <div class="gg-icon-button gg-settings-icon">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                <path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69-.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.65.07.97l-2.11 1.65c-.19.15-.24.42-.12-.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65z"/>
                            </svg>
                        </div>
                        <div class="gg-settings-content">
                            <div class="gg-settings-section">
                                <div class="gg-settings-title">Display Options</div>
                                <label class="gg-toggle ${toggleStates.official ? "active" : ""}" title="Toggle Official Stores">
                                    <input type="checkbox" id="gg-toggle-official-compact" ${toggleStates.official ? "checked" : ""}>
                                    <label>Official</label>
                                </label>
                                <label class="gg-toggle ${toggleStates.keyshop ? "active" : ""}" title="Toggle Keyshops">
                                    <input type="checkbox" id="gg-toggle-keyshop-compact" ${toggleStates.keyshop ? "checked" : ""}>
                                    <label>Keyshops</label>
                                </label>
                                <label class="gg-toggle ${toggleStates.compact ? "active" : ""}" title="Toggle Compact View">
                                    <input type="checkbox" id="gg-toggle-compact-menu" ${toggleStates.compact ? "checked" : ""}>
                                    <label>Compact</label>
                                </label>
                                <label class="gg-toggle ${toggleStates.subDisplay ? "active" : ""}" title="Toggle Sub/Bundle Displays">
                                    <input type="checkbox" id="gg-toggle-sub-display-compact" ${toggleStates.subDisplay ? "checked" : ""}>
                                    <label>Bundle Display</label>
                                </label>
                                <label class="gg-toggle ${toggleStates.enableScraping ? "active" : ""}" title="Enable/disable web scraping for non-API requests">
                                    <input type="checkbox" id="gg-toggle-enable-scraping-compact" ${toggleStates.enableScraping ? "checked" : ""}>
                                    <label>Enable Scraping</label>
                                </label>
                            </div>
                            <div class="gg-settings-section">
                                <div class="gg-settings-title">API Settings</div>
                                <label class="gg-toggle ${toggleStates.useApi ? "active" : ""}" title="Use GG.deals API">
                                    <input type="checkbox" id="gg-toggle-use-api-compact" ${toggleStates.useApi ? "checked" : ""}>
                                    <label>Use API</label>
                                </label>
                                <div>
                                    <div class="gg-api-key-wrapper">
                                        <input type="password" id="gg-api-key-compact" class="gg-api-key-input" 
                                               placeholder="Enter your GG.deals API key" value="${apiKey}">
                                        <button type="button" class="gg-toggle-visibility" title="Show/Hide API Key">
                                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                                <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
                                            </svg>
                                        </button>
                                    </div>
                                    <select id="gg-region-select-compact" class="gg-region-select">
                                        ${availableRegions.map(region => 
                                            `<option value="${region.code}" ${region.code === preferredRegion ? 'selected' : ''}>${region.name}</option>`
                                        ).join('')}
                                    </select>
                                    <div class="gg-api-status ${apiKey && toggleStates.useApi ? "active" : "inactive"}">
                                        API: ${apiKey && toggleStates.useApi ? "Active" : "Inactive"}
                                    </div>
                                    <button id="gg-save-api-key-compact" class="gg-save-button">Save Settings</button>
                                </div>
                            </div>
                            <div class="gg-settings-section">
                                <div class="gg-settings-title">Color Settings</div>
                                <div class="gg-color-grid">
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Background</div>
                                        <input type="color" id="gg-color-background-compact" class="gg-color-input" value="${savedColors.background}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Header Background</div>
                                        <input type="color" id="gg-color-header-bg-compact" class="gg-color-input" value="${savedColors.headerBackground}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Official Text</div>
                                        <input type="color" id="gg-color-official-text-compact" class="gg-color-input" value="${savedColors.officialText}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Official Price</div>
                                        <input type="color" id="gg-color-official-price-compact" class="gg-color-input" value="${savedColors.officialPrice}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Keyshop Text</div>
                                        <input type="color" id="gg-color-keyshop-text-compact" class="gg-color-input" value="${savedColors.keyshopText}">
                                    </div>
                                     <div class="gg-color-item">
                                        <div class="gg-color-label">Keyshop Price</div>
                                        <input type="color" id="gg-color-keyshop-price-compact" class="gg-color-input" value="${savedColors.keyshopPrice}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Best Price</div>
                                        <input type="color" id="gg-color-best-price-compact" class="gg-color-input" value="${savedColors.bestPrice}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Button Background</div>
                                        <input type="text" id="gg-color-button-bg-compact" class="gg-api-key-input" value="${savedColors.buttonBackground}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Button Text</div>
                                        <input type="color" id="gg-color-button-text-compact" class="gg-color-input" value="${savedColors.buttonText}">
                                    </div>
                                    <div class="gg-color-item">
                                        <div class="gg-color-label">Border Color</div>
                                        <input type="color" id="gg-color-border-compact" class="gg-color-input" value="${savedColors.borderColor.replace('30', '')}">
                                    </div>
                                </div>
                                <button id="gg-save-colors-compact" class="gg-save-button">Save Colors</button>
                                <button id="gg-reset-colors-compact" class="gg-reset-colors">Reset to Default</button>
                            </div>
                        </div>
                    </div>
                    <a href="#" target="_blank" class="gg-view-offers">View Offers</a>
                </div>
            </div>
            <div class="gg-price-sections">
                <div class="gg-price-section ${
                  toggleStates.official ? "" : "hidden"
                }" id="gg-official-section">
                    <div class="gg-price-left">
                        <span class="gg-price-label">
                            <img src="https://gg.deals/favicon.ico" class="gg-icon">
                            Official Stores
                        </span>
                    </div>
                    <div class="gg-price-info">
                        <span class="gg-price-value" id="gg-official-price">Loading...</span>
                        <span class="gg-price-value historical" id="gg-official-historical"></span>
                    </div>
                </div>
                <div class="gg-price-section ${
                  toggleStates.keyshop ? "" : "hidden"
                }" id="gg-keyshop-section">
                    <div class="gg-price-left">
                        <span class="gg-price-label">
                            <img src="https://gg.deals/favicon.ico" class="gg-icon">
                            Keyshops
                        </span>
                    </div>
                    <div class="gg-price-info">
                        <span class="gg-price-value" id="gg-keyshop-price">Loading...</span>
                        <span class="gg-price-value historical" id="gg-keyshop-historical"></span>
                    </div>
                </div>
            </div>
            <div class="gg-controls">
                <div class="gg-main-actions">
                    <a href="#" target="_blank" class="gg-view-offers">View Offers</a>
                    <button class="gg-icon-button gg-refresh" title="Refresh Prices">
                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
                        </svg>
                        <span class="gg-tooltip-text">Click to refresh prices</span>
                    </button>
                </div>
                <div class="gg-settings-panels">
                    <div class="gg-settings-section">
                        <div class="gg-settings-title">Display Options</div>
                        <label class="gg-toggle ${toggleStates.official ? "active" : ""}" title="Toggle Official Stores">
                            <input type="checkbox" id="gg-toggle-official" ${toggleStates.official ? "checked" : ""}>
                            <label>Official</label>
                        </label>
                        <label class="gg-toggle ${toggleStates.keyshop ? "active" : ""}" title="Toggle Keyshops">
                            <input type="checkbox" id="gg-toggle-keyshop" ${toggleStates.keyshop ? "checked" : ""}>
                            <label>Keyshops</label>
                        </label>
                        <label class="gg-toggle ${toggleStates.compact ? "active" : ""}" title="Toggle Compact View">
                            <input type="checkbox" id="gg-toggle-compact" ${toggleStates.compact ? "checked" : ""}>
                            <label>Compact</label>
                        </label>
                        <label class="gg-toggle ${toggleStates.subDisplay ? "active" : ""}" title="Toggle Sub/Bundle Displays">
                            <input type="checkbox" id="gg-toggle-sub-display" ${toggleStates.subDisplay ? "checked" : ""}>
                            <label>Bundle Display</label>
                        </label>
                        <label class="gg-toggle ${toggleStates.enableScraping ? "active" : ""}" title="Enable/disable web scraping for non-API requests">
                            <input type="checkbox" id="gg-toggle-enable-scraping" ${toggleStates.enableScraping ? "checked" : ""}>
                            <label>Enable Scraping</label>
                        </label>
                    </div>
                    <div class="gg-settings-section">
                        <div class="gg-settings-title">API Settings</div>
                        <label class="gg-toggle ${toggleStates.useApi ? "active" : ""}" title="Use GG.deals API">
                            <input type="checkbox" id="gg-toggle-use-api" ${toggleStates.useApi ? "checked" : ""}>
                            <label>Use API</label>
                        </label>
                        <div>
                            <div class="gg-api-key-wrapper">
                                <input type="password" id="gg-api-key" class="gg-api-key-input" 
                                       placeholder="Enter your GG.deals API key" value="${apiKey}">
                                <button type="button" class="gg-toggle-visibility" title="Show/Hide API Key">
                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                        <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
                                    </svg>
                                </button>
                            </div>
                            <select id="gg-region-select" class="gg-region-select">
                                ${availableRegions.map(region => 
                                    `<option value="${region.code}" ${region.code === preferredRegion ? 'selected' : ''}>${region.name}</option>`
                                ).join('')}
                            </select>
                            <div class="gg-api-status ${apiKey && toggleStates.useApi ? "active" : "inactive"}">
                                API: ${apiKey && toggleStates.useApi ? "Active" : "Inactive"}
                            </div>
                            <button id="gg-save-api-key" class="gg-save-button">Save Settings</button>
                        </div>
                    </div>
                    <div class="gg-settings-section">
                        <div class="gg-settings-title">Color Settings</div>
                        <div class="gg-color-grid">
                            <div class="gg-color-item">
                                <div class="gg-color-label">Background</div>
                                <input type="color" id="gg-color-background" class="gg-color-input" value="${savedColors.background}">
                            </div>
                            <div class="gg-color-item">
                                <div class="gg-color-label">Header Background</div>
                                <input type="color" id="gg-color-header-bg" class="gg-color-input" value="${savedColors.headerBackground}">
                            </div>
                            <div class="gg-color-item">
                                <div class="gg-color-label">Official Text</div>
                                <input type="color" id="gg-color-official-text" class="gg-color-input" value="${savedColors.officialText}">
                            </div>
                            <div class="gg-color-item">
                                <div class="gg-color-label">Official Price</div>
                                <input type="color" id="gg-color-official-price" class="gg-color-input" value="${savedColors.officialPrice}">
                            </div>
                            <div class="gg-color-item">
                                <div class="gg-color-label">Keyshop Text</div>
                                <input type="color" id="gg-color-keyshop-text" class="gg-color-input" value="${savedColors.keyshopText}">
                            </div>
                             <div class="gg-color-item">
                                <div class="gg-color-label">Keyshop Price</div>
                                <input type="color" id="gg-color-keyshop-price" class="gg-color-input" value="${savedColors.keyshopPrice}">
                            </div>
                            <div class="gg-color-item">
                                <div class="gg-color-label">Best Price</div>
                                <input type="color" id="gg-color-best-price" class="gg-color-input" value="${savedColors.bestPrice}">
                            </div>
                             <div class="gg-color-item">
                                <div class="gg-color-label">Button Background</div>
                                <input type="text" id="gg-color-button-bg" class="gg-api-key-input" value="${savedColors.buttonBackground}">
                            </div>
                            <div class="gg-color-item">
                                <div class="gg-color-label">Button Text</div>
                                <input type="color" id="gg-color-button-text" class="gg-color-input" value="${savedColors.buttonText}">
                            </div>
                            <div class="gg-color-item">
                                <div class="gg-color-label">Border Color</div>
                                <input type="color" id="gg-color-border" class="gg-color-input" value="${savedColors.borderColor.replace('30', '')}">
                            </div>
                        </div>
                        <button id="gg-save-colors" class="gg-save-button">Save Colors</button>
                        <button id="gg-reset-colors" class="gg-reset-colors">Reset to Default</button>
                    </div>
                </div>
            </div>
            <div class="gg-attribution">
        Extension by <a href="https://steamcommunity.com/profiles/76561199186030286">Crimsab</a> 
        <a href="https://github.com/Crimsab/ggdeals-steam-companion" title="View on GitHub">
            <svg class="github-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
                <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
            </svg>
        </a> · 
        Data by <a href="https://gg.deals">gg.deals</a>
        <span id="gg-api-indicator" class="gg-api-status ${toggleStates.useApi && apiKey ? "active" : "inactive"}" style="margin-left: 5px; display: inline-block;">
            ${toggleStates.useApi && apiKey ? "· API Active" : ""}
        </span>
    </div>
        `;

    // Add toggle listeners for both sets of controls
    const toggleOfficialCompact = container.querySelector(
      "#gg-toggle-official-compact"
    );
    const toggleKeyshopCompact = container.querySelector(
      "#gg-toggle-keyshop-compact"
    );
    const toggleCompactMenu = container.querySelector(
      "#gg-toggle-compact-menu"
    );
    const toggleOfficial = container.querySelector("#gg-toggle-official");
    const toggleKeyshop = container.querySelector("#gg-toggle-keyshop");
    const toggleCompact = container.querySelector("#gg-toggle-compact");
    const toggleSubDisplay = container.querySelector("#gg-toggle-sub-display");

    function updateToggleState(type, checked) {
      toggleStates[type] = checked;
      if (type === "compact") {
        GM_setValue("compactView", checked);
      } else if (type === "useApi") {
        GM_setValue("useApi", checked);
      } else if (type === "enableScraping") {
        // Make sure to explicitly save enableScraping setting
        GM_setValue("enableScraping", checked);
        console.log(`GG.deals: Saving enableScraping = ${checked}`);
      } else {
        GM_setValue(`show${type.charAt(0).toUpperCase() + type.slice(1)}`, checked);
      }

      if (type === "official" || type === "keyshop") {
        container.querySelector(`#gg-compact-${type}`).style.display = checked
          ? ""
          : "none";
        container
          .querySelector(`#gg-${type}-section`)
          .classList.toggle("hidden", !checked);
      } else if (type === "compact") {
        // Update all containers on the page, preserving sub-display containers
        document.querySelectorAll('.gg-deals-container').forEach(cont => {
          // Skip sub-display containers if we're switching to full view
          if (!checked && cont.classList.contains('bundle-sub-display')) {
            return;
          }
          cont.classList.toggle("compact", checked);
        });
      } else if (type === "subDisplay") {
        document.querySelectorAll('.gg-deals-container.bundle-sub-display').forEach(el => {
          el.style.display = checked ? "" : "none";
        });
      }

      // Update all related toggle buttons
      document.querySelectorAll(`input[id*=toggle-${type}]`).forEach((input) => {
        input.checked = checked;
        input.closest(".gg-toggle").classList.toggle("active", checked);
      });
    }

    // Add event listeners for all toggles
    [toggleOfficialCompact, toggleOfficial].forEach((toggle) => {
      if (toggle) {
        toggle.addEventListener("change", (e) =>
          updateToggleState("official", e.target.checked)
        );
      }
    });

    [toggleKeyshopCompact, toggleKeyshop].forEach((toggle) => {
      if (toggle) {
        toggle.addEventListener("change", (e) =>
          updateToggleState("keyshop", e.target.checked)
        );
      }
    });

    [toggleCompactMenu, toggleCompact].forEach((toggle) => {
      if (toggle) {
        toggle.addEventListener("change", (e) =>
          updateToggleState("compact", e.target.checked)
        );
      }
    });

    const toggleSubDisplayCompact = container.querySelector("#gg-toggle-sub-display-compact");
    [toggleSubDisplay, toggleSubDisplayCompact].forEach((toggle) => {
      if (toggle) {
        toggle.addEventListener("change", (e) => updateToggleState("subDisplay", e.target.checked));
      }
    });
    
    // Add web scraping toggle event listeners
    const toggleEnableScraping = container.querySelector("#gg-toggle-enable-scraping");
    const toggleEnableScrapingCompact = container.querySelector("#gg-toggle-enable-scraping-compact");
    [toggleEnableScraping, toggleEnableScrapingCompact].forEach((toggle) => {
      if (toggle) {
        toggle.addEventListener("change", (e) => {
          updateToggleState("enableScraping", e.target.checked);
        });
      }
    });
    
    // Add API toggle event listeners
    const toggleUseApi = container.querySelector("#gg-toggle-use-api");
    const toggleUseApiCompact = container.querySelector("#gg-toggle-use-api-compact");
    [toggleUseApi, toggleUseApiCompact].forEach((toggle) => {
      if (toggle) {
        toggle.addEventListener("change", (e) => {
          updateToggleState("useApi", e.target.checked);
          // Update API status text in settings
          document.querySelectorAll('.gg-api-status:not(#gg-api-indicator)').forEach(status => {
            status.classList.toggle('active', e.target.checked && apiKey);
            status.classList.toggle('inactive', !e.target.checked || !apiKey);
            status.textContent = `API: ${e.target.checked && apiKey ? "Active" : "Inactive"}`;
          });
          
          // Update API indicator in attribution
          const apiIndicator = document.getElementById('gg-api-indicator');
          if (apiIndicator) {
            apiIndicator.classList.toggle('active', e.target.checked && apiKey);
            apiIndicator.classList.toggle('inactive', !e.target.checked || !apiKey);
            apiIndicator.textContent = e.target.checked && apiKey ? "· API Active" : "";
          }
        });
      }
    });
    
    // Add API key visibility toggle listeners
    const toggleVisibilityBtns = container.querySelectorAll(".gg-toggle-visibility");
    toggleVisibilityBtns.forEach(btn => {
      if (btn) {
        btn.addEventListener("click", (e) => {
          // Stop event propagation to prevent closing the settings dropdown
          e.stopPropagation();
          // Find the corresponding input field (sibling of parent element)
          const inputField = btn.closest(".gg-api-key-wrapper").querySelector(".gg-api-key-input");
          if (inputField) {
            // Toggle between password and text type
            inputField.type = inputField.type === "password" ? "text" : "password";
            
            // Update the icon to reflect the current state
            const eyeIcon = btn.querySelector("svg");
            if (eyeIcon) {
              if (inputField.type === "password") {
                // Show the "eye" icon to indicate the user can click to show the password
                eyeIcon.innerHTML = '<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>';
              } else {
                // Show the "eye-off" icon to indicate the user can click to hide the password
                eyeIcon.innerHTML = '<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/>';
              }
            }
          }
        });
      }
    });
    
    // Add save API key button event listeners
    const saveApiKeyBtn = container.querySelector("#gg-save-api-key");
    const saveApiKeyBtnCompact = container.querySelector("#gg-save-api-key-compact");
    const apiKeyInput = container.querySelector("#gg-api-key");
    const apiKeyInputCompact = container.querySelector("#gg-api-key-compact");
    const regionSelect = container.querySelector("#gg-region-select");
    const regionSelectCompact = container.querySelector("#gg-region-select-compact");
    
    [saveApiKeyBtn, saveApiKeyBtnCompact].forEach(btn => {
      if (btn) {
        btn.addEventListener("click", () => {
          // Get value from the inputs in the same container
          const isCompact = btn.id.includes('compact');
          const input = isCompact ? apiKeyInputCompact : apiKeyInput;
          const regionInput = isCompact ? regionSelectCompact : regionSelect;
          const newApiKey = input.value.trim();
          const newRegion = regionInput.value;
          
          // Save to both inputs
          if (apiKeyInput) apiKeyInput.value = newApiKey;
          if (apiKeyInputCompact) apiKeyInputCompact.value = newApiKey;
          
          // Update region selectors
          if (regionSelect && regionSelectCompact) {
            regionSelect.value = newRegion;
            regionSelectCompact.value = newRegion;
          }
          
          // Save to GM storage
          GM_setValue("apiKey", newApiKey);
          GM_setValue("preferredRegion", newRegion);
          
          // Update global variables
          window.ggDealsApiKey = newApiKey;
          window.ggDealsRegion = newRegion;
          
          // Clear cache to reflect updated region
          document.querySelectorAll('.gg-deals-container').forEach(container => {
            if (container.id) {
              const match = container.id.match(/gg-deals-(app|sub|bundle)-(\d+)/);
              if (match) {
                const [, containerType, containerId] = match;
                priceCache.get(`${containerType}_${containerId}`, true);
              }
            }
          });
          
          // Update status in settings
          document.querySelectorAll('.gg-api-status:not(#gg-api-indicator)').forEach(status => {
            const isActive = toggleStates.useApi && newApiKey;
            status.classList.toggle('active', isActive);
            status.classList.toggle('inactive', !isActive);
            status.textContent = `API: ${isActive ? "Active" : "Inactive"}`;
          });
          
          // Update API indicator in attribution
          const apiIndicator = document.getElementById('gg-api-indicator');
          if (apiIndicator) {
            const isActive = toggleStates.useApi && newApiKey;
            apiIndicator.classList.toggle('active', isActive);
            apiIndicator.classList.toggle('inactive', !isActive);
            apiIndicator.textContent = isActive ? "· API Active" : "";
          }
          
          // Refresh prices with new region
          if (toggleStates.useApi && newApiKey) {
            document.querySelectorAll('.gg-refresh').forEach(refreshBtn => {
              refreshBtn.click();
            });
          }
        });
      }
    });

    // Add color setting event listeners
    const saveColorsBtn = container.querySelector("#gg-save-colors");
    const saveColorsBtnCompact = container.querySelector("#gg-save-colors-compact");
    const resetColorsBtn = container.querySelector("#gg-reset-colors");
    const resetColorsBtnCompact = container.querySelector("#gg-reset-colors-compact");
    
    // Function to get all color inputs
    function getAllColorInputs(compact = false) {
      const suffix = compact ? "-compact" : "";
      return {
        background: container.querySelector(`#gg-color-background${suffix}`),
        headerBackground: container.querySelector(`#gg-color-header-bg${suffix}`),
        officialText: container.querySelector(`#gg-color-official-text${suffix}`),
        officialPrice: container.querySelector(`#gg-color-official-price${suffix}`),
        keyshopText: container.querySelector(`#gg-color-keyshop-text${suffix}`),
        keyshopPrice: container.querySelector(`#gg-color-keyshop-price${suffix}`), // Added keyshopPrice
        bestPrice: container.querySelector(`#gg-color-best-price${suffix}`),
        buttonBackground: container.querySelector(`#gg-color-button-bg${suffix}`), // Added buttonBackground
        buttonText: container.querySelector(`#gg-color-button-text${suffix}`),
        borderColor: container.querySelector(`#gg-color-border${suffix}`)
      };
    }
    
    // Function to save colors from a set of inputs
    function saveColorsFromInputs(compact = false) {
      const inputs = getAllColorInputs(compact);
      const nonCompactInputs = getAllColorInputs(false);
      const compactInputs = getAllColorInputs(true);
      
      // Get the values from the inputs
      Object.keys(inputs).forEach(key => {
        if (inputs[key]) {
          let value = inputs[key].value;
          
          // Special handling for border color (add transparency)
          if (key === 'borderColor') {
            // Convert hex to hex with alpha
            value = value.replace('#', '#') + '30';
          }
          
          // Save to storage
          savedColors[key] = value;
          GM_setValue(`color_${key}`, value);
          
          // Update both sets of inputs
          if (nonCompactInputs[key]) {
            nonCompactInputs[key].value = inputs[key].value;
          }
          if (compactInputs[key]) {
            compactInputs[key].value = inputs[key].value;
          }
        }
      });
      
      // Apply the new colors
      applyCustomColors();
    }
    
    // Function to reset colors to default
    function resetColorsToDefault() {
      // Reset all colors to default
      Object.keys(defaultColors).forEach(key => {
        savedColors[key] = defaultColors[key];
        GM_setValue(`color_${key}`, defaultColors[key]);
      });
      
      // Update all inputs
      const nonCompactInputs = getAllColorInputs(false);
      const compactInputs = getAllColorInputs(true);
      
      Object.keys(defaultColors).forEach(key => {
        let displayValue = defaultColors[key];
        
        // Special handling for border color (remove transparency for display)
        // For button background, which is a text input, just use the value directly.
        if (key === 'borderColor') {
          displayValue = defaultColors[key].replace('30', '');
        }
        
        if (nonCompactInputs[key]) {
          nonCompactInputs[key].value = displayValue;
        }
        if (compactInputs[key]) {
          compactInputs[key].value = displayValue;
        }
      });
      
      // Apply the default colors
      applyCustomColors();
    }
    
    // Add save colors button listeners
    [saveColorsBtn, saveColorsBtnCompact].forEach(btn => {
      if (btn) {
        btn.addEventListener("click", () => {
          const isCompact = btn.id.includes('compact');
          saveColorsFromInputs(isCompact);
        });
      }
    });
    
    // Add reset colors button listeners
    [resetColorsBtn, resetColorsBtnCompact].forEach(btn => {
      if (btn) {
        btn.addEventListener("click", resetColorsToDefault);
      }
    });

    // Add refresh button listeners to both compact and full view buttons
    container.querySelectorAll(".gg-refresh").forEach(refreshButton => {
      const refreshText = refreshButton.querySelector(".gg-tooltip-text");
      refreshButton.addEventListener("click", async function () {
        refreshButton.classList.add("loading");
        refreshButton.disabled = true;

        try {
          // First check if this container has its own ID/type information
          let type, id;
          
          if (container.id) {
            const containerMatch = container.id.match(/gg-deals-(app|sub|bundle)-(\d+)/);
            if (containerMatch) {
              [, type, id] = containerMatch;
            }
          }
          
          // If no container-specific info, use the URL
          if (!type || !id) {
            const urlMatch = window.location.pathname.match(/\/(app|sub|bundle)\/(\d+)/);
            if (urlMatch) {
              [, type, id] = urlMatch;
            } else {
              throw new Error("Could not determine game ID");
            }
          }
          
          // Force a refresh by clearing the cache first
          const cacheKey = `${type}_${id}`;
          priceCache.get(cacheKey, true);
          
          // Fetch fresh data
          await fetchGamePrices(null, container.id, true, { type, id });
          
          // Check the data source after update
          const dataSource = priceCache.getSource(`${type}_${id}`) || "web";
          const sourceText = dataSource === "api" ? "API" : "Web";
          
          refreshText.textContent = `Updated just now (via ${sourceText})`;
          setTimeout(() => {
            refreshText.textContent = "";
          }, 3000);
        } catch (error) {
          console.error("Failed to refresh prices:", error);
          refreshText.textContent = "Refresh failed";
          setTimeout(() => {
            refreshText.textContent = "";
          }, 3000);
        } finally {
          refreshButton.classList.remove("loading");
          refreshButton.disabled = false;
        }
      });
    });

    // Add settings dropdown toggle
    const settingsIcon = container.querySelector(".gg-settings-icon");
    const settingsContent = container.querySelector(".gg-settings-content");

    settingsIcon.addEventListener("click", (e) => {
      e.stopPropagation();
      settingsContent.classList.toggle("show");
    });

    // Close settings dropdown when clicking outside
    document.addEventListener("click", (e) => {
      if (!settingsContent.contains(e.target) && !settingsIcon.contains(e.target)) {
        settingsContent.classList.remove("show");
      }
    });

    // Update last refresh time if cached data exists
    const urlMatch = window.location.pathname.match(/\/(app|sub|bundle)\/(\d+)/);
    if (urlMatch) {
      const [, type, id] = urlMatch;
      const timestamp = priceCache.getTimestamp(`${type}_${id}`);
      if (timestamp) {
                  // Update all refresh tooltips with the timestamp and source
          container.querySelectorAll('.gg-refresh').forEach(refreshButton => {
            const tooltipSpan = refreshButton.querySelector('.gg-tooltip-text');
            if (tooltipSpan) {
              const timeAgo = Math.floor((Date.now() - timestamp) / 60000); // minutes
              
              // Get source of data (API or web scraping)
              const source = priceCache.getSource(`${type}_${id}`) || "web";
              const sourceText = source === "api" ? "API" : "Web";
              
              // Format time ago text
              let timeText;
              if (timeAgo < 60) {
                timeText = `${timeAgo}m ago`;
              } else {
                const hoursAgo = Math.floor(timeAgo / 60);
                timeText = `${hoursAgo}h ago`;
              }
              
              tooltipSpan.textContent = `Updated ${timeText} (via ${sourceText})`;
            }
          });
      }
    }

    return container;
  }

  // Improved error handling and retries
  async function fetchWithRetry(url, retries = MAX_RETRIES) {
    try {
      const response = await rateLimitedRequest(url);
      if (response.status === 200) {
        return response;
      }
      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      if (retries > 0) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return fetchWithRetry(url, retries - 1);
      }
      throw error;
    }
  }

  async function fetchGamePrices(gameTitle, containerId, forceRefresh = false, idInfo = null) {
    let type, id;

    if (idInfo) {
        type = idInfo.type;
        id = idInfo.id;
    } else {
        // First try to get ID from the container itself
        const container = document.getElementById(containerId);
        if (container) {
            const purchaseGame = container.closest('.game_area_purchase_game');
            if (purchaseGame) {
                const bundleInput = purchaseGame.querySelector('input[name="bundleid"]');
                const subInput = purchaseGame.querySelector('input[name="subid"]');
                if (bundleInput) {
                    type = 'bundle';
                    id = bundleInput.value;
                } else if (subInput) {
                    type = 'sub';
                    id = subInput.value;
                }
            }
        }

        // If no ID found from container, try URL
        if (!type || !id) {
            const urlMatch = window.location.pathname.match(/\/(app|sub|bundle)\/(\d+)/);
            if (!urlMatch) {
                console.warn("GG.deals: Could not find Steam ID");
                return;
            }
            [, type, id] = urlMatch;
        }
    }

    const cacheKey = `${type}_${id}`;
    
    // Track active requests to prevent duplicate requests for the same game
    const pendingRequestKey = `pending_${cacheKey}`;
    if (window[pendingRequestKey] && !forceRefresh) {
        console.log(`GG.deals: Request for ${type} ${id} already in progress, waiting...`);
        try {
            await window[pendingRequestKey];
            // After the pending request completes, get the cached data
            const cachedAfterWait = priceCache.get(cacheKey);
            if (cachedAfterWait) {
                updatePriceDisplay(cachedAfterWait, containerId);
                return;
            }
        } catch (error) {
            console.warn(`GG.deals: Error waiting for pending request: ${error}`);
            // Continue with a new request if the pending one failed
        }
    }

    // Check cache before making any requests
    const cachedData = priceCache.get(cacheKey, forceRefresh);
    if (cachedData) {
        updatePriceDisplay(cachedData, containerId);
        return;
    }

    // Create a promise for this request that other potential duplicate requests can await
    const requestPromise = (async () => {
        try {
            // If forcing refresh, clear cache for all containers on the page
            if (forceRefresh) {
                document.querySelectorAll('.gg-deals-container').forEach(container => {
                    if (container.id && container.id !== containerId) {
                        const match = container.id.match(/gg-deals-(app|sub|bundle)-(\d+)/);
                        if (match) {
                            const [, containerType, containerId] = match;
                            priceCache.get(`${containerType}_${containerId}`, true);
                        }
                    }
                });
            }
            
            // Check if API should be used
            const useApi = GM_getValue("useApi", false);
            const apiKey = GM_getValue("apiKey", "");
            const enableScraping = GM_getValue("enableScraping", true);
            
            // Try to use API if enabled and key is available
            // Note: API works for app IDs and sub IDs, not bundle IDs
            // Bundle pages always use web scraping regardless of API settings
            if (useApi && apiKey && (type === 'app' || type === 'sub')) {
                try {
                    // Use the ID directly, whether it's an app or sub ID
                    const itemId = id;
                    console.log(`GG.deals: Using API to fetch prices for ${type} ${id}`);
                    const data = await fetchGamePricesApi(itemId, type);
                    if (data) {
                        priceCache.set(cacheKey, data, "api");
                        updatePriceDisplay(data, containerId);
                        return;
                    }
                } catch (error) {
                    console.warn("GG.deals API fetch failed:", error);
                    // If API fails and scraping is disabled, hide the container
                    if (!enableScraping) {
                        const noDataResult = {
                            officialPrice: "No data",
                            keyshopPrice: "No data",
                            historicalData: [],
                            lowestPriceType: null,
                            url: `https://gg.deals/steam/${type}/${id}/`,
                            isCorrectGame: true,
                            noData: true // Flag to indicate there's no data
                        };
                        priceCache.set(cacheKey, noDataResult, "api");
                        // Hide the container instead of updating with "No data"
                        const container = document.getElementById(containerId);
                        if (container) {
                            container.style.display = "none";
                        }
                        return;
                    }
                    // Otherwise fall back to web scraping if enabled
                }
            }

            // If scraping is disabled, hide the container entirely
            if (!enableScraping) {
                const noDataResult = {
                    officialPrice: "No data",
                    keyshopPrice: "No data",
                    historicalData: [],
                    lowestPriceType: null,
                    url: `https://gg.deals/steam/${type}/${id}/`,
                    isCorrectGame: true,
                    noData: true // Flag to indicate there's no data
                };
                priceCache.set(cacheKey, noDataResult, "web");
                // Hide the container instead of updating with "No data"
                const container = document.getElementById(containerId);
                if (container) {
                    container.style.display = "none";
                }
                return;
            }

            // If we get here, we're either:
            // 1. Using web scraping for any type (scraping enabled)
            // 2. Not using API at all

            // Batch requests for performance
            await new Promise(resolve => setTimeout(resolve, Math.random() * 100));

            // Function to convert game name to URL slug
            const toUrlSlug = (name) => {
                return name.toLowerCase()
                    .replace(/[^a-z0-9]+/g, '-')
                    .replace(/^-+|-+$/g, '');
            };

            // Define base URL formats - only try one format unless necessary
            let urlFormats = [];
            
            if (type === 'bundle') {
                // For bundles, only try the bundle format
                urlFormats = [{ type: 'bundle', id: id }];
            } else if (type === 'app') {
                // For apps, only try the app format
                urlFormats = [{ type: 'app', id: id }];
            } else if (type === 'sub') {
                // For subs, try both sub and app formats since GG.deals sometimes has subs under app URLs
                urlFormats = [
                    { type: 'sub', id: id },
                    { type: 'app', id: id } // Try app format as fallback
                ];
            }

            // Try each URL format
            for (const format of urlFormats) {
                try {
                    const steamUrl = `https://gg.deals/steam/${format.type}/${format.id}/`;
                    const response = await fetchWithRetry(steamUrl);
                    const data = extractPriceData(response.responseText);
                    if (data && data.officialPrice !== "No data") {
                        priceCache.set(cacheKey, data, "web");
                        updatePriceDisplay(data, containerId);
                        return;
                    }
                } catch (error) {
                    console.warn(`GG.deals ${format.type} URL fetch failed:`, error);
                }
            }

            // If the direct Steam URL didn't work, hide the container
            const noDataResult = {
                officialPrice: "No data",
                keyshopPrice: "No data",
                historicalData: [],
                lowestPriceType: null,
                url: `https://gg.deals/steam/${type}/${id}/`,
                isCorrectGame: true,
                noData: true // Flag to indicate there's no data
            };
            
            priceCache.set(cacheKey, noDataResult, "web");
            // Hide the container
            const container = document.getElementById(containerId);
            if (container) {
                container.style.display = "none";
            }
        } finally {
            // Clear the pending request marker when done
            window[pendingRequestKey] = null;
        }
    })();
    
    // Store the promise so other requests for the same item can wait for it
    window[pendingRequestKey] = requestPromise;
    
    // Execute the promise
    await requestPromise;
  }

  function extractPriceData(html, expectedGameName) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, "text/html");

    // Get the actual game name from the page
    const pageGameName = doc.querySelector('.game-info-title')?.textContent?.trim() ||
                        doc.querySelector('.game-header-title')?.textContent?.trim();

    // Check if we got the correct game
    const isCorrectGame = !expectedGameName || !pageGameName || 
                         pageGameName.toLowerCase().includes(expectedGameName.toLowerCase()) ||
                         expectedGameName.toLowerCase().includes(pageGameName.toLowerCase());

    // Check if it's a valid game page
    if (!doc.querySelector('.game-info-price-col')) {
        return {
            officialPrice: "No data",
            keyshopPrice: "No data",
            historicalData: [],
            lowestPriceType: null,
            url: doc.querySelector('link[rel="canonical"]')?.href || "https://gg.deals",
            isCorrectGame
        };
    }

    // Find current prices (non-historical)
    let officialPrice = "No data";
    let keyshopPrice = "No data";

    // Look for current prices in the main price sections (not historical)
    const currentPriceSections = Array.from(doc.querySelectorAll('.game-info-price-col')).filter(
      section => !section.classList.contains('historical')
    );

    currentPriceSections.forEach(section => {
      const label = section.querySelector('.game-info-price-label')?.textContent.trim();
      const price = section.querySelector('.price-inner.numeric')?.textContent.trim();
      
      if (label?.includes('Official Stores')) {
        officialPrice = price || "No data";
      } else if (label?.includes('Keyshops')) {
        keyshopPrice = price || "No data";
      }
    });

    // Historical lows (separate section)
    const historicalPrices = doc.querySelectorAll(
      ".game-info-price-col.historical.game-header-price-box"
    );
    const historicalData = [];
    historicalPrices.forEach((priceBox) => {
      const label = priceBox
        .querySelector(".game-info-price-label")
        ?.textContent.trim();
      const price = priceBox
        .querySelector(".price-inner.numeric")
        ?.textContent.trim();
      let date = priceBox
        .querySelector(".game-price-active-label")
        ?.textContent.trim();
      date = date?.replace("Expired", "").trim();

      if (!price || !date) return;

      const historicalText = `Historical Low: ${price} (${date})`;

      if (label?.includes("Official Stores Low")) {
        historicalData.push({
          type: "official",
          price: price,
          historical: historicalText,
        });
      } else if (label?.includes("Keyshops Low") && keyshopPrice !== "No data") {
        historicalData.push({
          type: "keyshop",
          price: price,
          historical: historicalText,
        });
      }
    });

    // Compare current prices (not historical) to determine the lowest
    const officialPriceNum = parseFloat(
      officialPrice.replace(/[^0-9,.]/g, "").replace(",", ".")
    );
    const keyshopPriceNum = parseFloat(
      keyshopPrice.replace(/[^0-9,.]/g, "").replace(",", ".")
    );

    let lowestPriceType = null;
    if (!isNaN(officialPriceNum) && !isNaN(keyshopPriceNum)) {
      lowestPriceType =
        officialPriceNum <= keyshopPriceNum ? "official" : "keyshop";
    } else if (!isNaN(officialPriceNum)) {
      lowestPriceType = "official";
    } else if (!isNaN(keyshopPriceNum)) {
      lowestPriceType = "keyshop";
    }

    // Get the current URL for the "View Offers" link
    const currentUrl = doc.querySelector('link[rel="canonical"]')?.href || "https://gg.deals";

    return {
      officialPrice: officialPrice,
      keyshopPrice: keyshopPrice,
      historicalData: historicalData,
      lowestPriceType: lowestPriceType,
      url: currentUrl,
      isCorrectGame
    };
  }

  function updatePriceDisplay(data, containerId) {
    const container = document.getElementById(containerId);
    if (!container) return;
    
    // If data has the noData flag, hide the container and return
    if (data && data.noData) {
        container.style.display = "none";
        return;
    }

    // Update all View Offers links in the container
    const links = container.querySelectorAll(".gg-view-offers");

    if (data) {
        // Update prices based on container type
        if (container.classList.contains('bundle-sub-display')) {
            // Update compact display
            const officialPrice = container.querySelector('.gg-compact-official-price');
            const keyshopPrice = container.querySelector('.gg-compact-keyshop-price');
            const officialHistorical = container.querySelector('.gg-compact-official-historical');
            const keyshopHistorical = container.querySelector('.gg-compact-keyshop-historical');

            if (officialPrice) officialPrice.textContent = data.officialPrice;
            if (keyshopPrice) keyshopPrice.textContent = data.keyshopPrice;

            // Show historical data regardless of current price status
            if (officialHistorical) {
                const officialHistData = data.historicalData.find(h => h.type === 'official');
                officialHistorical.textContent = officialHistData?.historical || '';
            }
            if (keyshopHistorical) {
                const keyshopHistData = data.historicalData.find(h => h.type === 'keyshop');
                keyshopHistorical.textContent = keyshopHistData?.historical || '';
            }

            // Update best price indicators
            if (officialPrice) officialPrice.classList.remove('best-price');
            if (keyshopPrice) keyshopPrice.classList.remove('best-price');

            if (data.lowestPriceType === 'official' && officialPrice) {
                officialPrice.classList.add('best-price');
            } else if (data.lowestPriceType === 'keyshop' && keyshopPrice) {
                keyshopPrice.classList.add('best-price');
            }
        } else {
            // Update full display
            const elements = {
                official: {
                    price: container.querySelector("#gg-official-price"),
                    historical: container.querySelector("#gg-official-historical"),
                    compactPrice: container.querySelector("#gg-compact-official-price"),
                    compactHistorical: container.querySelector("#gg-compact-official-historical")
                },
                keyshop: {
                    price: container.querySelector("#gg-keyshop-price"),
                    historical: container.querySelector("#gg-keyshop-historical"),
                    compactPrice: container.querySelector("#gg-compact-keyshop-price"),
                    compactHistorical: container.querySelector("#gg-compact-keyshop-historical")
                }
            };

            // Update prices
            if (elements.official.price) elements.official.price.textContent = data.officialPrice;
            if (elements.keyshop.price) elements.keyshop.price.textContent = data.keyshopPrice;
            if (elements.official.compactPrice) elements.official.compactPrice.textContent = data.officialPrice;
            if (elements.keyshop.compactPrice) elements.keyshop.compactPrice.textContent = data.keyshopPrice;

            // Update historical data regardless of current price status
            const officialHistData = data.historicalData.find(h => h.type === 'official');
            const keyshopHistData = data.historicalData.find(h => h.type === 'keyshop');

            if (elements.official.historical) {
                elements.official.historical.textContent = officialHistData?.historical || '';
            }
            if (elements.keyshop.historical) {
                elements.keyshop.historical.textContent = keyshopHistData?.historical || '';
            }
            if (elements.official.compactHistorical) {
                elements.official.compactHistorical.textContent = officialHistData?.historical || '';
            }
            if (elements.keyshop.compactHistorical) {
                elements.keyshop.compactHistorical.textContent = keyshopHistData?.historical || '';
            }

            // Update best price indicators
            [elements.official.price, elements.official.compactPrice, elements.keyshop.price, elements.keyshop.compactPrice].forEach(el => {
                if (el) el.classList.remove('best-price');
            });

            if (data.lowestPriceType === 'official') {
                [elements.official.price, elements.official.compactPrice].forEach(el => {
                    if (el) el.classList.add('best-price');
                });
            } else if (data.lowestPriceType === 'keyshop') {
                [elements.keyshop.price, elements.keyshop.compactPrice].forEach(el => {
                    if (el) el.classList.add('best-price');
                });
            }
        }

        // Update all View Offers links
        if (data.url) {
            links.forEach(link => {
                link.href = data.url;
            });
        }
    } else {
        // Handle error state
        const priceElements = container.querySelectorAll('.gg-price-value:not(.historical)');
        priceElements.forEach(el => {
            el.textContent = 'Not found';
        });

        const historicalElements = container.querySelectorAll('.gg-historical-tooltip-text, .gg-price-value.historical');
        historicalElements.forEach(el => {
            el.textContent = '';
        });

        // Set default URL for all View Offers links
        links.forEach(link => {
            link.href = `https://gg.deals/steam/${type}/${id}/`;
        });
    }
  }

  function createCompactPriceDisplay(containerId) {
    const container = document.createElement('div');
    container.className = 'gg-deals-container compact bundle-sub-display';
    container.id = containerId;
    container.style.display = toggleStates.subDisplay ? "" : "none";
    container.innerHTML = `
        <div class="gg-compact-row">
            <img src="https://gg.deals/favicon.ico" alt="GG.deals" class="gg-icon">
            <div class="gg-compact-prices">
                <div class="gg-compact-price-item gg-compact-official" style="${!toggleStates.official ? "display:none" : ""}">
                    <span>Official:</span>
                    <span class="gg-historical-tooltip">
                        <span class="gg-price-value gg-compact-official-price">Loading...</span>
                        <span class="gg-historical-tooltip-text gg-compact-official-historical"></span>
                    </span>
                </div>
                <div class="gg-compact-price-item gg-compact-keyshop" style="${!toggleStates.keyshop ? "display:none" : ""}">
                    <span>Keyshop:</span>
                    <span class="gg-historical-tooltip">
                        <span class="gg-price-value gg-compact-keyshop-price">Loading...</span>
                        <span class="gg-historical-tooltip-text gg-compact-keyshop-historical"></span>
                    </span>
                </div>
            </div>
            <div class="gg-compact-controls">
                <a href="#" target="_blank" class="gg-view-offers">View Offers</a>
            </div>
        </div>
    `;
    return container;
  }

  // Wait for Steam page to fully load (including age gate) and handle tab visibility
  let isInitialized = false;
  
  // Queue for batching price requests
  const requestQueue = {
    items: [],
    processing: false,
    
    add: function(item) {
      this.items.push(item);
      if (!this.processing) {
        this.processQueue();
      }
    },
    
    processQueue: async function() {
      if (this.items.length === 0) {
        this.processing = false;
        return;
      }
      
      this.processing = true;
      
      // Process items in batches of 3 with a small delay between batches
      const BATCH_SIZE = 3;
      const BATCH_DELAY = 300; // ms
      
      // Process a batch
      const batch = this.items.splice(0, BATCH_SIZE);
      const promises = batch.map(item => {
        return fetchGamePrices(
          item.gameTitle, 
          item.containerId, 
          item.forceRefresh, 
          item.idInfo
        ).catch(err => {
          console.warn(`GG.deals: Error processing queue item:`, err);
          // Update the display to show error
          const container = document.getElementById(item.containerId);
          if (container) {
            const priceElements = container.querySelectorAll('.gg-price-value:not(.historical)');
            priceElements.forEach(el => {
              el.textContent = 'Error';
            });
          }
        });
      });
      
      // Wait for batch to complete
      await Promise.all(promises);
      
      // Small delay before next batch to avoid overwhelming browser
      if (this.items.length > 0) {
        setTimeout(() => this.processQueue(), BATCH_DELAY);
      } else {
        this.processing = false;
      }
    }
  };

  function initializeWhenVisible() {
    if (document.visibilityState === "visible" && !isInitialized) {
        const urlMatch = window.location.pathname.match(/\/(app|sub|bundle)\/(\d+)/);
        if (!urlMatch) return;

        const [, pageType, pageId] = urlMatch;
        isInitialized = true;

        // For app pages, show the full container at the top
        if (pageType === 'app') {
            const purchaseSection = document.querySelector("#game_area_purchase");
            if (purchaseSection) {
                const mainContainer = createPriceContainer();
                mainContainer.id = 'gg-deals-main';
                purchaseSection.parentNode.insertBefore(mainContainer, purchaseSection);
                
                // Prioritize the main container request
                requestQueue.add({
                  gameTitle: null,
                  containerId: 'gg-deals-main',
                  forceRefresh: false,
                  idInfo: { type: pageType, id: pageId }
                });
            }
        }

        // For sub/bundle pages, show only one display at the top
        if (pageType === 'sub' || pageType === 'bundle') {
            // Try to find the first purchase game section
            const firstPurchaseGame = document.querySelector('.game_area_purchase_game');
            if (firstPurchaseGame) {
                const mainContainer = createPriceContainer();
                mainContainer.id = `gg-deals-${pageType}-${pageId}`;
                firstPurchaseGame.parentNode.insertBefore(mainContainer, firstPurchaseGame);
                
                requestQueue.add({
                  gameTitle: null,
                  containerId: mainContainer.id,
                  forceRefresh: false,
                  idInfo: { type: pageType, id: pageId }
                });
            }
            return; // Exit early to prevent additional displays
        }

        // Only process additional items if we're on an app page
        if (pageType === 'app') {
            // Get all purchase sections up front
            const purchaseSections = document.querySelectorAll('.game_area_purchase_game');
            let delayIndex = 0;
            
            // Function to process sections with delay
            const processSections = () => {
              // Process each purchase game with the bundle/sub displays
              purchaseSections.forEach((element, index) => {
                // Skip if this is a demo section
                if (element.closest('.demo_above_purchase')) {
                    return;
                }
            
                // Get the ID and type from the inputs
                const bundleInput = element.querySelector('input[name="bundleid"]');
                const subInput = element.querySelector('input[name="subid"]');
            
                if (!bundleInput && !subInput) {
                    // If no inputs found, try to get ID from the element ID
                    const elementId = element.id.match(/\d+$/)?.[0];
            
                    // Skip main app on app pages (main app already handled above)
                    if (pageType === 'app' && elementId === pageId) {
                        return;
                    }
                }

                let itemType, itemId;
                if (bundleInput) {
                    itemType = 'bundle';
                    itemId = bundleInput.value;
                } else if (subInput) {
                    itemType = 'sub';
                    itemId = subInput.value;
                } else {
                    // Skip this item if we can't identify it
                    return;
                }

                const containerId = `gg-deals-${itemType}-${itemId}`;
                
                // Check if a display already exists for this ID
                if (document.getElementById(containerId)) {
                  return; // Skip if already exists
                }
                
                const compactDisplay = createCompactPriceDisplay(containerId);
                
                // Insert before game_purchase_action
                const purchaseAction = element.querySelector('.game_purchase_action');
                if (purchaseAction) {
                    purchaseAction.parentNode.insertBefore(compactDisplay, purchaseAction);
                    
                    // Add to request queue with lower priority
                    requestQueue.add({
                      gameTitle: null, 
                      containerId: containerId,
                      forceRefresh: false,
                      idInfo: { type: itemType, id: itemId }
                    });
                }
              });
            };
            
            // Use requestIdleCallback if available, otherwise use setTimeout
            if (typeof window.requestIdleCallback === 'function') {
              window.requestIdleCallback(() => processSections(), { timeout: 1000 });
            } else {
              // Small delay to let the main page render first
              setTimeout(processSections, 200);
            }
        }
    }
  }

  // Utility function to debounce function calls - useful for event handlers
  function debounce(func, wait) {
    let timeout;
    return function(...args) {
      const context = this;
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(context, args), wait);
    };
  }

  // Check for visibility changes with debounce
  document.addEventListener("visibilitychange", debounce(initializeWhenVisible, 100));

  // Initial check (in case the tab is already visible)
  // Use setTimeout to ensure the page is fully loaded
  setTimeout(() => {
    if (document.visibilityState === "visible") {
      initializeWhenVisible();
    }
  }, 500);
  
  // Cleanup interval check after a reasonable time
  const checkTitle = setInterval(() => {
    if (document.visibilityState === "visible") {
      initializeWhenVisible();
      if (isInitialized) {
        clearInterval(checkTitle);
      }
    }
  }, 1000);
  
  // Clear the interval after 10 seconds regardless to avoid resource waste
  setTimeout(() => {
    clearInterval(checkTitle);
  }, 10000);
})();