// 馃憢 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.0
// @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/
// @downloadURL none
// ==/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_CLASS = "SortAndFilters-module__filterList___T81LH";
const BUTTON_APPLY_FILTERS_CLASS =
"ApplyFiltersButton-module__applyButton___faTvE";
const BUTTON_SHOW_FILTERS_CLASS = "SortAndFilters-module__button___OeFeU";
const LOAD_MORE_ROW_CLASS = "BrowsePage-module__loadMoreRow___sx0qx";
// Esperar a que exista el elemento filterList
const filterList = await waitForElement(`.${FILTER_LIST_CLASS}`);
// 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 = `
`;
const svgCollapsed = `
`;
li.innerHTML = `
${options
.map(
(opt, index) => `
`
)
.join("")}
${additionalContent}
`;
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();
saveFilterState();
};
// 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();
saveFilterState();
});
});
}
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 = `
M谩s de
Menos de
`;
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: `% 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"));
}
});
}
// Funciones para manejar persistencia
let currentFilterState = {
priceMin: "",
priceMax: "",
free: true,
paid: true,
unpurchasable: true,
discountGroup: "none",
customDiscountPercent: "",
gamePassOnly: false,
};
function saveFilterState() {
currentFilterState = {
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,
};
}
function loadFilterState() {
if (!currentFilterState) return;
if (document.querySelector("#priceMin"))
document.querySelector("#priceMin").value = currentFilterState.priceMin;
if (document.querySelector("#priceMax"))
document.querySelector("#priceMax").value = currentFilterState.priceMax;
if (document.querySelector("#free_checkbox"))
document.querySelector("#free_checkbox").checked =
currentFilterState.free;
if (document.querySelector("#paid_checkbox"))
document.querySelector("#paid_checkbox").checked =
currentFilterState.paid;
if (document.querySelector("#unpurchasable_checkbox"))
document.querySelector("#unpurchasable_checkbox").checked =
currentFilterState.unpurchasable;
if (
currentFilterState.discountGroup !== "none" &&
document.querySelector(`#${currentFilterState.discountGroup}_radio`)
) {
document.querySelector(
`#${currentFilterState.discountGroup}_radio`
).checked = true;
}
if (document.querySelector("#customDiscountPercent"))
document.querySelector("#customDiscountPercent").value =
currentFilterState.customDiscountPercent;
if (document.querySelector("#gamePassOnly_checkbox"))
document.querySelector("#gamePassOnly_checkbox").checked =
currentFilterState.gamePassOnly;
}
// Observer para mantener los filtros en m贸vil
function setupFilterListObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
const filterList = document.querySelector(`.${FILTER_LIST_CLASS}`);
if (filterList && !filterList.hasAttribute("data-initialized")) {
initializeFilters(filterList);
filterList.setAttribute("data-initialized", "true");
}
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
function initializeFilters(filterList) {
// Definir las funciones de filtro antes de usarlas
const priceFilterFn = () => {
applyAllFilters();
saveFilterState();
return true;
};
const offersFilterFn = () => {
applyAllFilters();
saveFilterState();
return true;
};
// Crear los filtros
const priceFilter = createFilter(
"PriceRange",
"Precio",
[
{
id: "free",
label: "Mostrar Gratis",
defaultSelected: currentFilterState.free,
},
{
id: "paid",
label: "Mostrar de Pago",
defaultSelected: currentFilterState.paid,
},
{
id: "unpurchasable",
label: "Mostrar Incomprable",
defaultSelected: currentFilterState.unpurchasable,
},
],
`