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-26 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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.8.15
// @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*
// @match           https://www.xbox.com/*/Search/Result*
// @license         MIT
// @supportURL      https://gist.githubusercontent.com/JLCareglio/9cbddea558658f695983a64b9cece6a6/
// ==/UserScript==

// MARK: CONSTANTS
const SELECTORS = {
  GAME_PASS_DISCOUNT: "Price-module__afterPriceTextContainer___r7fdq",
  DISCOUNT_TAG: "ProductCard-module__discountTag___OjGFy",
  FINAL_PRICE: "ProductCard-module__price___cs1xr",
  FILTER_LIST: "SortAndFilters-module__filterList___T81LH",
  BUTTON_APPLY_FILTERS: "ApplyFiltersButton-module__applyButton___faTvE",
  BUTTON_SHOW_FILTERS: "SortAndFilters-module__button___OeFeU",
  LOAD_MORE_ROW: "BrowsePage-module__loadMoreRow___sx0qx",
  XBOX_SPINNER_FILTER: "XboxSpinner-module__xenonFilter___lbfRN",
};

// MARK: DOM HELPER
const DOMHelper = {
  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,
      });
    });
  },
};

// MARK: FILTER CREATOR
const FilterCreator = {
  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;
  },

  addFilterHandlers(filterElement, filterFn, stateManager) {
    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;

      const toggleFilter = () => {
        const isSelected = option.getAttribute("aria-selected") === "true";
        const newState = !isSelected;

        if (input.type === "radio") {
          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();
        stateManager.save();
      };

      option.addEventListener("click", (e) => {
        if (e.target !== input) {
          e.preventDefault();
          toggleFilter();
        }
      });

      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();
        stateManager.save();
      });
    });
  },
};

// MARK: FILTER LOGIC
const FilterLogic = {
  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;

    const customDiscountPercent = parseInt(
      document.querySelector("#customDiscountPercent")?.value || "0"
    );

    productCards.forEach((card) => {
      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;

      const priceElement = card.querySelector(`.${SELECTORS.FINAL_PRICE}`);
      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 priceString = priceText.trim().replace(/[^\d.,]/g, "");
            let price;

            const match = priceString.match(/^(.*?)[\.,](\d{2})$/);
            if (match) {
              const integerPart = match[1].replace(/[.,]/g, "");
              const decimalPart = match[2];
              price = parseFloat(`${integerPart}.${decimalPart}`);
            } else {
              price = parseFloat(priceString.replace(/[.,]/g, ""));
            }
            shouldShow =
              !isNaN(price) &&
              price >= minPrice &&
              (maxPrice === 0 || price <= maxPrice);
          }
        }
      }

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

        if (onlyGamePass && selectedDiscountFilter !== "none") {
          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";
    });
  },
};

// MARK: STATE MANAGER
class FilterStateManager {
  constructor() {
    this.state = {
      priceMin: "",
      priceMax: "",
      free: true,
      paid: true,
      unpurchasable: true,
      discountGroup: "none",
      customDiscountPercent: "",
      gamePassOnly: false,
    };
  }

  save() {
    this.state = {
      priceMin: document.querySelector("#priceMin")?.value || "",
      priceMax: document.querySelector("#priceMax")?.value || "",
      free: document.querySelector("#free_checkbox")?.checked || false,
      paid: document.querySelector("#paid_checkbox")?.checked || false,
      unpurchasable:
        document.querySelector("#unpurchasable_checkbox")?.checked || false,
      discountGroup:
        document.querySelector('input[name="discountGroup"]:checked')?.value ||
        "none",
      customDiscountPercent:
        document.querySelector("#customDiscountPercent")?.value || "",
      gamePassOnly:
        document.querySelector("#gamePassOnly_checkbox")?.checked || false,
    };
  }

  load() {
    if (!this.state) return;

    if (document.querySelector("#priceMin"))
      document.querySelector("#priceMin").value = this.state.priceMin;
    if (document.querySelector("#priceMax"))
      document.querySelector("#priceMax").value = this.state.priceMax;
    if (document.querySelector("#free_checkbox"))
      document.querySelector("#free_checkbox").checked = this.state.free;
    if (document.querySelector("#paid_checkbox"))
      document.querySelector("#paid_checkbox").checked = this.state.paid;
    if (document.querySelector("#unpurchasable_checkbox"))
      document.querySelector("#unpurchasable_checkbox").checked =
        this.state.unpurchasable;
    if (
      this.state.discountGroup !== "none" &&
      document.querySelector(`#${this.state.discountGroup}_radio`)
    ) {
      document.querySelector(
        `#${this.state.discountGroup}_radio`
      ).checked = true;
    }
    if (document.querySelector("#customDiscountPercent"))
      document.querySelector("#customDiscountPercent").value =
        this.state.customDiscountPercent;
    if (document.querySelector("#gamePassOnly_checkbox"))
      document.querySelector("#gamePassOnly_checkbox").checked =
        this.state.gamePassOnly;
  }
}

// MARK: MAIN
class XboxStoreFilter {
  constructor() {
    this.isInitialized = false;
    this.stateManager = new FilterStateManager();
    this.lastFilterList = null;
  }

  async initialize() {
    const filterList = await DOMHelper.waitForElement(
      `.${SELECTORS.FILTER_LIST}`
    );
    if (filterList) {
      this.setupFilters(filterList);
      this.setupObservers();
      this.setupEventListeners();
    }
  }

  setupFilters(filterList) {
    const existingPriceFilter = filterList.querySelector("#PriceRange");
    const existingOffersFilter = filterList.querySelector("#Offers");
    if (existingPriceFilter) existingPriceFilter.closest("li")?.remove();
    if (existingOffersFilter) existingOffersFilter.closest("li")?.remove();

    const priceFilterFn = () => {
      FilterLogic.applyAllFilters();
      this.stateManager.save();
      return true;
    };

    const offersFilterFn = () => {
      FilterLogic.applyAllFilters();
      this.stateManager.save();
      return true;
    };

    const priceFilter = FilterCreator.createFilter(
      "PriceRange",
      "Precio",
      [
        {
          id: "free",
          label: "Mostrar Gratis",
          defaultSelected: this.stateManager.state.free,
        },
        {
          id: "paid",
          label: "Mostrar de Pago",
          defaultSelected: this.stateManager.state.paid,
        },
        {
          id: "unpurchasable",
          label: "Mostrar Incomprable",
          defaultSelected: this.stateManager.state.unpurchasable,
        },
      ],
      `
      <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 offersFilter = FilterCreator.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);

    FilterCreator.addFilterHandlers(
      priceFilter,
      priceFilterFn,
      this.stateManager
    );
    FilterCreator.addFilterHandlers(
      offersFilter,
      offersFilterFn,
      this.stateManager
    );

    const priceInputs = document.querySelectorAll("#priceMin, #priceMax");
    priceInputs.forEach((input) => {
      input.addEventListener("change", () => {
        FilterLogic.applyAllFilters();
        this.stateManager.save();
      });
    });

    document
      .querySelector("#customDiscountPercent")
      ?.addEventListener("change", (e) => {
        const radio = document.querySelector("#discountCustom_radio");
        if (radio) {
          radio.checked = true;
          radio.dispatchEvent(new Event("change"));
          this.stateManager.save();
        }
      });

    this.stateManager.load();
    FilterLogic.applyAllFilters();
  }

  setupObservers() {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === "childList") {
          const currentFilterList = document.querySelector(
            `.${SELECTORS.FILTER_LIST}`
          );

          // Si aparece una nueva lista de filtros y es diferente a la última observada
          if (currentFilterList && currentFilterList !== this.lastFilterList) {
            this.lastFilterList = currentFilterList;
            this.setupFilters(currentFilterList);
            this.isInitialized = true;
          }

          // Si desaparece la lista de filtros, reiniciamos para la próxima
          if (!document.querySelector(`.${SELECTORS.FILTER_LIST}`)) {
            this.isInitialized = false;
            this.lastFilterList = null;
          }
        }
      });
    });

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

  setupEventListeners() {
    const applyButton = document.querySelector(
      `.${SELECTORS.BUTTON_APPLY_FILTERS}`
    );
    if (applyButton) {
      applyButton.addEventListener("click", () => {
        FilterLogic.applyAllFilters();
        this.stateManager.save();
      });
    }
  }
}

// MARK: INITIALIZE APP
(async () => {
  const app = new XboxStoreFilter();
  await app.initialize();
})();