Greasy Fork

Greasy Fork is available in English.

Xbox Store Price & Deals Filter

Add price range filters and deal filters to Xbox store. Filter by Game Pass discounts, specific discount percentages, and price ranges.

当前为 2025-01-25 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// 👋 Hola, usa 🐒Tampermonkey 👇
// https://www.tampermonkey.net/

// ==UserScript==
// @name            Xbox Store Price & Deals Filter
// @name:es         Filtros de Precio y Ofertas para Xbox Store
// @name:it         Filtri Prezzi e Offerte per Xbox Store
// @name:fr         Filtres de Prix et Offres pour Xbox Store
// @name:de         Preis- und Angebotsfilter für Xbox Store
// @namespace       https://jlcareglio.github.io/
// @version         0.7.2
// @description     Add price range filters and deal filters to Xbox store. Filter by Game Pass discounts, specific discount percentages, and price ranges.
// @description:es  Agrega filtros de rango de precios y ofertas a la tienda Xbox. Filtra por descuentos de Game Pass, porcentajes específicos de descuento y rangos de precios.
// @description:it  Aggiunge filtri per fascia di prezzo e offerte allo store Xbox. Filtra per sconti Game Pass, percentuali di sconto specifiche e fasce di prezzo.
// @description:fr  Ajoute des filtres de gamme de prix et d'offres au magasin Xbox. Filtre par réductions Game Pass, pourcentages de réduction spécifiques et gammes de prix.
// @description:de  Fügt Preisbereich- und Angebotsfilter zum Xbox-Store hinzu. Filtert nach Game Pass-Rabatten, spezifischen Rabattprozenten und Preisbereichen.
// @icon            https://www.google.com/s2/favicons?sz=64&domain=xbox.com
// @grant           none
// @author          Jesús Lautaro Careglio Albornoz
// @source          https://gist.githubusercontent.com/JLCareglio/9cbddea558658f695983a64b9cece6a6/raw/
// @match           https://www.xbox.com/*/games/all-games*
// @match           https://www.xbox.com/*/games/browse*
// @supportURL      https://gist.githubusercontent.com/JLCareglio/9cbddea558658f695983a64b9cece6a6/
// ==/UserScript==

(async () => {
  // Función para esperar a que un elemento exista
  function waitForElement(selector) {
    return new Promise((resolve) => {
      if (document.querySelector(selector)) {
        return resolve(document.querySelector(selector));
      }

      const observer = new MutationObserver(() => {
        if (document.querySelector(selector)) {
          observer.disconnect();
          resolve(document.querySelector(selector));
        }
      });

      observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
      });
    });
  }

  /* Significado de las distintas clases que pueden estar en algún lugar dentro de una ProductCard de un juego:
    - "Price-module__afterPriceTextContainer___r7fdq": se puede comprar a un precio reducido si se tiene una suscripción activa de gamePass
    - "ProductCard-module__discountTag___OjGFy" indica que tiene un descuento y el porcentaje esta en su innerText
    - "ProductCard-module__price___cs1xr" su innerText contiene el precio final en formato local, por ejemplo, puede tener "ARS$ 19.999,20" por lo que se tiene que convertir al flotante 19999.20 pero si no contiene ningún numero, por ejemplo "Gratis+" o "Free" es porque el producto es gratis y la ausencia de esta clase indica que el producto NO se puede comprar o adquirir
  */
  const GAME_PASS_DISCOUNT_CLASS =
    "Price-module__afterPriceTextContainer___r7fdq";
  const DISCOUNT_TAG_CLASS = "ProductCard-module__discountTag___OjGFy";
  const FINAL_PRICE_CLASS = "ProductCard-module__price___cs1xr";
  const FILTER_LIST = "SortAndFilters-module__filterList___T81LH";

  // Esperar a que exista el elemento filterList
  const filterList = await waitForElement(`.${FILTER_LIST}`);

  // El resto del código continúa igual...
  if (filterList) {
    // Modificar la función createFilter para aceptar inputs adicionales
    function createFilter(id, title, options, additionalContent = "") {
      const li = document.createElement("li");
      li.className = "SortAndFilters-module__li___aV+Oo";

      const svgExpanded = `
        <svg width="1em" height="1em" viewBox="0 0 32 32" aria-hidden="true" class="SelectionDropdown-module__chevronIcon___Z81Q8" style="transform: rotate(180deg);">
        <path d="M1.132 10.277a1.125 1.125 0 011.493-.088l.098.088 13.381 13.38 13.349-13.349a1.125 1.125 0 011.492-.087l.099.087c.408.408.437 1.051.087 1.493l-.087.098-14.145 14.145a1.125 1.125 0 01-1.493.087l-.098-.087L1.132 11.868a1.125 1.125 0 010-1.591z" fill-rule="evenodd"></path>
        </svg>
      `;

      const svgCollapsed = `
        <svg width="1em" height="1em" viewBox="0 0 32 32" aria-hidden="true" class="SelectionDropdown-module__chevronIcon___Z81Q8">
        <path d="M1.132 10.277a1.125 1.125 0 011.493-.088l.098.088 13.381 13.38 13.349-13.349a1.125 1.125 0 011.492-.087l.099.087c.408.408.437 1.051.087 1.493l-.087.098-14.145 14.145a1.125 1.125 0 01-1.493.087l-.098-.087L1.132 11.868a1.125 1.125 0 010-1.591z" fill-rule="evenodd"></path>
        </svg>
      `;

      li.innerHTML = `
        <div class="SelectionDropdown-module__container___XzkIx" id="${id}">
          <button class="SelectionDropdown-module__titleContainer___YyoD0" aria-expanded="false">
            <span class="typography-module__xdsSubTitle2___6d6Da SelectionDropdown-module__titleText___PN6s9">${title}</span>
            <div class="SelectionDropdown-module__filterInfoContainer___7ktfT">
              ${svgExpanded}
            </div>
          </button>
          <div style="max-height: 20rem; overflow-y: auto" class="hidden">
            <ul class="Selections-module__options___I24e7" role="listbox">
              ${options
                .map(
                  (opt, index) => `
                <li>
                  <button class="Selections-module__selectionContainer___m2xzM" 
                          id="${opt.id}" 
                          role="${opt.type || "checkbox"}" 
                          aria-selected="${
                            opt.defaultSelected ? "true" : "false"
                          }" 
                          aria-checked="${
                            opt.defaultSelected ? "true" : "false"
                          }"
                          aria-label="${opt.label.replace(/<[^>]*>/g, "")}, ${
                    index + 1
                  } de ${options.length}">
                    <label class="Selections-module__icon___IBPqb">
                      <input type="${opt.type || "checkbox"}" 
                             id="${opt.id}_${opt.type || "checkbox"}" 
                             name="${opt.group || opt.id}" 
                             value="${opt.id}" 
                             ${opt.defaultSelected ? "checked" : ""}>
                      <span class="typography-module__xdsSubTitle2___6d6Da Selections-module__label___CpN0F Selections-module__textColor___CnMSs">
                        ${opt.label}
                      </span>
                    </label>
                  </button>
                </li>
              `
                )
                .join("")}
            </ul>
            ${additionalContent}
          </div>
        </div>
      `;

      return li;
    }

    function addFilterHandlers(filterElement, filterFn) {
      const button = filterElement.querySelector(
        ".SelectionDropdown-module__titleContainer___YyoD0"
      );
      const optionsDiv = filterElement.querySelector("div[style]");
      const options = filterElement.querySelectorAll(
        ".Selections-module__selectionContainer___m2xzM"
      );

      button.addEventListener("click", () => {
        const isExpanded = button.getAttribute("aria-expanded") === "true";
        button.setAttribute("aria-expanded", !isExpanded);
        optionsDiv.classList.toggle("hidden");
      });

      options.forEach((option) => {
        const input = option.querySelector("input");
        if (!input) return;

        // Función para manejar el cambio de estado
        const toggleFilter = () => {
          const isSelected = option.getAttribute("aria-selected") === "true";
          const newState = !isSelected;

          if (input.type === "radio") {
            // Para radio buttons, desmarcar todos los otros del mismo grupo
            options.forEach((opt) => {
              const otherInput = opt.querySelector('input[type="radio"]');
              if (otherInput && otherInput.name === input.name) {
                opt.setAttribute("aria-selected", "false");
                opt.setAttribute("aria-checked", "false");
                otherInput.checked = false;
              }
            });
          }

          option.setAttribute("aria-selected", newState);
          option.setAttribute("aria-checked", newState);
          input.checked = newState;

          // Aplicar filtro
          filterFn();
        };

        // Manejar click en el botón completo
        option.addEventListener("click", (e) => {
          if (e.target !== input) {
            e.preventDefault(); // Prevenir comportamiento por defecto
            toggleFilter();
          }
        });

        // Manejar cambios en el input
        input.addEventListener("change", () => {
          if (input.type === "radio") {
            options.forEach((opt) => {
              const otherInput = opt.querySelector('input[type="radio"]');
              if (otherInput && otherInput.name === input.name) {
                opt.setAttribute("aria-selected", otherInput.checked);
                opt.setAttribute("aria-checked", otherInput.checked);
              }
            });
          } else {
            option.setAttribute("aria-selected", input.checked);
            option.setAttribute("aria-checked", input.checked);
          }

          // Aplicar filtro
          filterFn();
        });
      });
    }

    function applyAllFilters() {
      const productCards = document.querySelectorAll(
        ".ProductCard-module__cardWrapper___6Ls86"
      );

      const selectedDiscountFilter =
        document.querySelector('input[name="discountGroup"]:checked')?.value ||
        "none";
      const onlyGamePass =
        document.querySelector("#gamePassOnly_checkbox")?.checked || false;

      // Obtener el valor personalizado de descuento
      const customDiscountPercent = parseInt(
        document.querySelector("#customDiscountPercent")?.value || "0"
      );

      productCards.forEach((card) => {
        // Obtener estados de los filtros
        const minPrice =
          parseFloat(document.querySelector("#priceMin").value) || 0;
        const maxPrice =
          parseFloat(document.querySelector("#priceMax").value) || Infinity;
        const showFree = document.querySelector("#free_checkbox").checked;
        const showPaid = document.querySelector("#paid_checkbox").checked;
        const showUnpurchasable = document.querySelector(
          "#unpurchasable_checkbox"
        ).checked;

        // Verificar precio
        const priceElement = card.querySelector(`.${FINAL_PRICE_CLASS}`);
        let shouldShow = true;

        if (!priceElement) {
          shouldShow = showUnpurchasable;
        } else {
          const priceText = priceElement.innerText;
          const hasNumbers = /\d/.test(priceText);
          const isFree = !hasNumbers;

          if (isFree) {
            shouldShow = showFree;
          } else {
            shouldShow = showPaid;
            if (shouldShow) {
              const price = parseFloat(
                priceText.replace(/[^0-9,]/g, "").replace(",", ".")
              );
              shouldShow =
                price >= minPrice && (maxPrice === 0 || price <= maxPrice);
            }
          }
        }

        // Verificar descuentos
        if (shouldShow) {
          const discountTag = card.querySelector(`.${DISCOUNT_TAG_CLASS}`);
          const hasGamePassDiscount = card.querySelector(
            `.${GAME_PASS_DISCOUNT_CLASS}`
          );
          const discountPercentage = discountTag
            ? parseInt(discountTag.innerText.replace(/[^0-9]/g, ""))
            : 0;

          if (onlyGamePass && selectedDiscountFilter !== "none") {
            // Primero verificamos si tiene descuento de Game Pass
            shouldShow = hasGamePassDiscount ? true : false;

            // Si tiene Game Pass, ahora verificamos el porcentaje de descuento
            if (shouldShow) {
              switch (selectedDiscountFilter) {
                case "anyDiscount":
                  shouldShow = discountTag !== null;
                  break;
                case "discount50plus":
                  shouldShow = discountPercentage >= 50;
                  break;
                case "discount75plus":
                  shouldShow = discountPercentage >= 75;
                  break;
                case "discountCustom":
                  shouldShow = discountPercentage >= customDiscountPercent;
                  break;
              }
            }
          } else if (onlyGamePass) {
            shouldShow = hasGamePassDiscount ? true : false;
          } else if (selectedDiscountFilter !== "none") {
            switch (selectedDiscountFilter) {
              case "anyDiscount":
                shouldShow = discountTag !== null;
                break;
              case "discount50plus":
                shouldShow = discountPercentage >= 50;
                break;
              case "discount75plus":
                shouldShow = discountPercentage >= 75;
                break;
              case "discountCustom":
                shouldShow = discountPercentage >= customDiscountPercent;
                break;
            }
          }
        }

        card.parentElement.style.display = shouldShow ? "" : "none";
      });
    }

    // Crear y añadir los filtros
    // Filtro de Precio con inputs personalizados
    const priceRangeInputs = `
      <div>
        <div style="display: flex; flex-direction: column; align-items: center">
          <span>Más de</span>
          <input type="number" id="priceMin" min="0" />
        </div>
        <div style="display: flex; flex-direction: column; align-items: center">
          <span>Menos de</span>
          <input type="number" id="priceMax" min="0" />
        </div>
      </div>
    `;

    const priceFilter = createFilter(
      "PriceRange",
      "Precio",
      [
        { id: "free", label: "Mostrar Gratis", defaultSelected: true },
        { id: "paid", label: "Mostrar de Pago", defaultSelected: true },
        {
          id: "unpurchasable",
          label: "Mostrar Incomprable",
          defaultSelected: true,
        },
      ],
      priceRangeInputs
    );

    // Modificar la creación del filtro de ofertas
    const offersFilter = createFilter("Offers", "Oferta", [
      {
        id: "anyDiscount",
        label: "Con descuento",
        type: "radio",
        group: "discountGroup",
      },
      {
        id: "discount50plus",
        label: "50% o más",
        type: "radio",
        group: "discountGroup",
      },
      {
        id: "discount75plus",
        label: "75% o más",
        type: "radio",
        group: "discountGroup",
      },
      {
        id: "discountCustom",
        label: `<input type="number" id="customDiscountPercent" min="0" max="100" style="width: 50px;">% o más`,
        type: "radio",
        group: "discountGroup",
      },
      { id: "gamePassOnly", label: "Solo con Game Pass", type: "checkbox" },
    ]);

    filterList.appendChild(priceFilter);
    filterList.appendChild(offersFilter);

    // Agregar handlers para los filtros
    const priceFilterFn = () => {
      applyAllFilters();
      return true; // Siempre retorna true ya que la lógica está en applyAllFilters
    };

    const offersFilterFn = () => {
      applyAllFilters();
      return true; // Siempre retorna true ya que la lógica está en applyAllFilters
    };

    addFilterHandlers(priceFilter, priceFilterFn);
    addFilterHandlers(offersFilter, offersFilterFn);

    // Agregar listeners para los inputs de precio
    const priceInputs = document.querySelectorAll("#priceMin, #priceMax");
    priceInputs.forEach((input) => {
      input.addEventListener("change", applyAllFilters);
    });

    // Agregar listener para el input de porcentaje personalizado
    document
      .querySelector("#customDiscountPercent")
      ?.addEventListener("change", (e) => {
        const radio = document.querySelector("#discountCustom_radio");
        if (radio) {
          radio.checked = true;
          radio.dispatchEvent(new Event("change"));
        }
      });
  }
})();