您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhanced Xbox-Now Filters with advanced pillbox UI for a seamless and intuitive experience
// ==UserScript== // @name Xbox-Now Enhanced Filters // @namespace https://jlcareglio.github.io/ // @version 3.6.1 // @description Enhanced Xbox-Now Filters with advanced pillbox UI for a seamless and intuitive experience // @author Jesús Lautaro Careglio Albornoz // @source https://gist.githubusercontent.com/JLCareglio/1e4b0838bdf31e21ed749cfcd89a3a47/raw/01_Xbox-Now-Enhanced-Filters.user.js // @match *://*.xbox-now.com/* // @license MIT // @compatible firefox // @compatible chrome // @compatible opera // @compatible safari // @compatible edge // @compatible brave // @icon https://www.google.com/s2/favicons?sz=64&domain=xbox-now.com // @grant none // @supportURL https://gist.github.com/JLCareglio/1e4b0838bdf31e21ed749cfcd89a3a47 // @require https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4 // ==/UserScript== (() => { "use strict"; const style = document.createElement("style"); style.textContent = ` .collapse { visibility: visible !important; } @keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .animate-fade-in { animation: fadeIn 0.2s ease-out; } @keyframes highlight-fade { from { background-color: #dcfce7; } to { background-color: #e5e7eb; } } .animate-highlight { animation: highlight-fade 1.5s ease-out forwards; } `; document.head.appendChild(style); const hideSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8/10 h-8/10"><path d="M3.53 2.47a.75.75 0 0 0-1.06 1.06l18 18a.75.75 0 1 0 1.06-1.06l-18-18ZM22.676 12.553a11.249 11.249 0 0 1-2.631 4.31l-3.099-3.099a5.25 5.25 0 0 0-6.71-6.71L7.759 4.577a11.217 11.217 0 0 1 4.242-.827c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113Z" /><path d="M15.75 12c0 .18-.013.357-.037.53l-4.244-4.243A3.75 3.75 0 0 1 15.75 12ZM12.53 15.713l-4.243-4.244a3.75 3.75 0 0 0 4.244 4.243Z" /><path d="M6.75 12c0-.619.107-1.213.304-1.764l-3.1-3.1a11.25 11.25 0 0 0-2.63 4.31c-.12.362-.12.752 0 1.114 1.489 4.467 5.704 7.69 10.675 7.69 1.5 0 2.933-.294 4.242-.827l-2.477-2.477A5.25 5.25 0 0 1 6.75 12Z" /></svg>'; const showSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8/10 h-8/10"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" /><path fill-rule="evenodd" d="M1.323 11.447C2.811 6.976 7.028 3.75 12.001 3.75c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113-1.487 4.471-5.705 7.697-10.677 7.697-4.97 0-9.186-3.223-10.675-7.69a1.762 1.762 0 0 1 0-1.113ZM17.25 12a5.25 5.25 0 1 1-10.5 0 5.25 5.25 0 0 1 10.5 0Z" clip-rule="evenodd" /></svg>'; const filterSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8/10 h-8/10"><path fill-rule="evenodd" d="M3.792 2.938A49.069 49.069 0 0 1 12 2.25c2.797 0 5.54.236 8.209.688a1.857 1.857 0 0 1 1.541 1.836v1.044a3 3 0 0 1-.879 2.121l-6.182 6.182a1.5 1.5 0 0 0-.439 1.061v2.927a3 3 0 0 1-1.658 2.684l-1.757.878A.75.75 0 0 1 9.75 21v-5.818a1.5 1.5 0 0 0-.44-1.06L3.13 7.938a3 3 0 0 1-.879-2.121V4.774c0-.897.64-1.683 1.542-1.836Z" clip-rule="evenodd" /></svg>'; const minimizeSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-9 h-9"><path fill-rule="evenodd" d="M12.53 16.28a.75.75 0 0 1-1.06 0l-7.5-7.5a.75.75 0 0 1 1.06-1.06L12 14.69l6.97-6.97a.75.75 0 1 1 1.06 1.06l-7.5 7.5Z" clip-rule="evenodd" /></svg>'; const cancelSVG = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-9 h-9"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>'; const defaultFilters = { includeTags: { enabled: true, tags: ["PC", "GAME PASS"] }, excludeTags: { enabled: false, tags: [] }, hiddenGames: { enabled: true, games: [] }, }; const processStoredFilters = () => { const storedFilters = localStorage.getItem("filters"); if (!storedFilters) return defaultFilters; try { const parsed = JSON.parse(storedFilters); ["includeTags", "excludeTags", "hiddenGames"].forEach((key) => { if (parsed[key] && parsed[key].enabled !== undefined) { if (Array.isArray(parsed[key].tags)) parsed[key].tags = parsed[key].tags.filter( (tag) => typeof tag === "string" ); else if (parsed[key].games) parsed[key].games = parsed[key].games.filter( (game) => typeof game === "string" ); } else parsed[key] = { ...defaultFilters[key] }; }); return parsed; } catch (e) { console.error("Error al procesar los filtros guardados:", e); return { ...defaultFilters }; } }; const filters = processStoredFilters(); function filterGames() { document .querySelectorAll(".box-body.comparison-table-entry") .forEach((gameEntry) => { const storeButton = gameEntry.querySelector("a.btn"); if (!storeButton) return; const labels = gameEntry.querySelectorAll(".label"); const hasTags = (tags) => Array.from(labels).some((label) => tags.some((tag) => label.textContent.toUpperCase().includes(tag.toUpperCase()) ) ); const isIncluded = !filters.includeTags.enabled || filters.includeTags.tags.length === 0 || hasTags(filters.includeTags.tags); const isExcluded = filters.excludeTags.enabled && filters.excludeTags.tags.length > 0 && hasTags(filters.excludeTags.tags); const isHidden = filters.hiddenGames.enabled && filters.hiddenGames.games.includes(storeButton.title); const shouldBeVisible = isIncluded && !isExcluded && !isHidden; gameEntry.style.display = shouldBeVisible ? "" : "none"; }); } document .querySelectorAll(".box-body.comparison-table-entry") .forEach((gameEntry) => { const storeButton = gameEntry.querySelector("a.btn"); const btnHide = document.createElement("button"); const btnConfig = document.createElement("button"); const btnContainer = document.createElement("div"); const gameName = storeButton.title; const isHidden = filters.hiddenGames.games.includes(gameName); btnHide.innerHTML = isHidden ? showSVG : hideSVG; btnHide.className = "flex-1 flex items-center justify-center h-full p-0 m-0 hover:bg-[#8d0040]"; btnConfig.innerHTML = filterSVG; btnConfig.className = "flex-1 flex items-center justify-center h-full p-0 m-0 hover:bg-[#8d0040]"; btnContainer.className = "text-white bg-[#a6004c] border border-[#8d0040] mt-2 flex h-[34px] w-full overflow-hidden [&>button:not(:last-child)]:border-r [&>button:not(:last-child)]:border-r-white"; btnContainer.appendChild(btnHide); btnContainer.appendChild(btnConfig); storeButton.parentNode.appendChild(btnContainer); btnHide.addEventListener("click", () => { const gameName = storeButton.title; const gameIndex = filters.hiddenGames.games.indexOf(gameName); if (gameIndex > -1) { filters.hiddenGames.games.splice(gameIndex, 1); btnHide.innerHTML = hideSVG; } else { filters.hiddenGames.games.push(gameName); btnHide.innerHTML = showSVG; } localStorage.setItem("filters", JSON.stringify(filters)); const hiddenGamesTextarea = document.getElementById("hiddenGames-values"); if (hiddenGamesTextarea) { hiddenGamesTextarea.value = filters.hiddenGames.games .map(escapeCommas) .join(","); hiddenGamesTextarea.dispatchEvent(new CustomEvent("renderpills")); } filterGames(); }); btnConfig.addEventListener("click", () => { const modal = document.getElementById("medium-modal"); modal.classList.remove("hidden"); }); }); function createFilterSection(id, title, description, placeholder) { const values = filters[id].tags || filters[id].games || []; const textareaValue = Array.isArray(values) ? values.join(", ") : ""; return ` <div class="bg-white rounded-xl shadow-sm p-4 border border-slate-200/80"> <div class="flex items-center justify-between"> <div class="flex items-center gap-3 cursor-pointer" onclick="document.getElementById('${id}-enabled').click()"> <h4 class="text-base font-semibold text-slate-800">${title}</h4> </div> <label class="relative inline-flex items-center cursor-pointer mb-0!"> <input type="checkbox" id="${id}-enabled" class="sr-only peer" ${ filters[id].enabled ? "checked" : "" }> <div class="w-15 h-9 bg-slate-200 rounded-full peer peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#a6004c]/30 peer-checked:after:translate-x-5.5 peer-checked:after:border-white after:content-[''] after:absolute after:top-[1.3px] after:left-[2.3px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-8 after:w-8 after:transition-all peer-checked:bg-[#a6004c]"></div> </label> </div> <p class="text-sm text-slate-500 mt-1 mb-3 font-normal">${description}</p> <div id="${id}-pills-container" class="w-full p-2.5 border border-slate-300 rounded-lg min-h-24 text-sm bg-slate-50 focus-within:ring-2 focus-within:ring-[#a6004c]/50 focus-within:border-[#a6004c] transition flex flex-wrap gap-2 items-start content-start overflow-y-auto"> <!-- Pills will be generated here --> </div> <textarea id="${id}-values" class="w-full p-2.5 border border-slate-300 rounded-lg h-24 text-sm bg-slate-50 focus:ring-2 focus:ring-[#a6004c]/50 focus:border-[#a6004c] transition hidden" placeholder="${placeholder}" >${textareaValue}</textarea> </div> `; } function initializePillInputs() { ["includeTags", "excludeTags", "hiddenGames"].forEach((id) => { const pillsContainer = document.getElementById(`${id}-pills-container`); const textarea = document.getElementById(`${id}-values`); let isEditing = false; let lastAdded = []; let renderPills; const toTextareaView = () => { if (isEditing) return; pillsContainer.classList.add("hidden"); textarea.classList.remove("hidden"); if ( textarea.value.trim().length > 0 && !textarea.value.trim().endsWith(",") ) textarea.value += ", "; textarea.focus(); textarea.selectionStart = textarea.selectionEnd = textarea.value.length; }; const createPill = (value) => { const pill = document.createElement("span"); pill.className = "inline-flex items-center bg-slate-200 text-slate-800 text-sm font-medium rounded-full overflow-hidden animate-fade-in"; pill.onclick = (e) => e.stopPropagation(); if (lastAdded.includes(value)) pill.classList.add("animate-highlight"); const pillText = document.createElement("span"); pillText.textContent = value; pillText.className = "cursor-pointer pl-2.5 pr-2 py-1 hover:bg-slate-300/60 transition-colors"; pillText.onclick = (e) => { e.stopPropagation(); editPill(pill); }; const deleteBtn = document.createElement("button"); deleteBtn.type = "button"; deleteBtn.className = "px-1.5 py-1 border-l border-slate-400/20 text-slate-500 hover:text-[#a6004c] hover:bg-slate-300/60 transition-colors self-stretch flex items-center"; deleteBtn.innerHTML = cancelSVG.replace("w-9 h-9", "w-4 h-4"); deleteBtn.onclick = (e) => { e.stopPropagation(); const currentValues = textarea.value.split(",").map((v) => v.trim()); const index = currentValues.indexOf(value); if (index > -1) { currentValues.splice(index, 1); textarea.value = currentValues.join(", "); } renderPills(); }; pill.appendChild(pillText); pill.appendChild(deleteBtn); return pill; }; const editPill = (pill) => { isEditing = true; const pillText = pill.querySelector("span"); const deleteBtn = pill.querySelector("button"); const currentValue = pillText.textContent; const input = document.createElement("input"); input.type = "text"; input.value = currentValue; input.className = "bg-transparent focus:outline-none w-auto py-1 pl-2.5 pr-2"; input.style.width = `${currentValue.length + 3}ch`; input.onclick = (e) => e.stopPropagation(); pillText.classList.add("hidden"); deleteBtn.classList.add("hidden"); pill.insertBefore(input, deleteBtn); input.focus(); const saveChanges = () => { isEditing = false; const newValue = input.value.trim().toUpperCase(); const currentValues = textarea.value.split(",").map((v) => v.trim()); const index = currentValues.indexOf(currentValue); if (newValue && newValue !== currentValue) { if (index > -1) { currentValues[index] = newValue; textarea.value = currentValues.join(", "); } } else if (!newValue) { if (index > -1) { currentValues.splice(index, 1); textarea.value = currentValues.join(", "); } } renderPills(); }; input.onblur = saveChanges; input.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); saveChanges(); } else if (e.key === "Escape") { e.preventDefault(); renderPills(); isEditing = false; } }; }; const escapeCommas = (str) => str.replace(/,/g, "\\,"); const unescapeCommas = (str) => str.replace(/\\,/g, ","); const splitByUnescapedCommas = (str) => { return str .split(/(?<!\\),/) .map((s) => s.trim()) .filter((s) => s.length > 0) .map((s) => s.replace(/\\,/g, ",")); }; const addValues = (inputValue) => { const normalizedInput = inputValue.replace(/\s*,\s*/g, ","); const newValues = splitByUnescapedCommas(normalizedInput) .map((v) => v.trim()) .filter((v) => v); if (newValues.length > 0) { const currentValues = textarea.value ? splitByUnescapedCommas(textarea.value) : []; lastAdded = newValues.filter( (v) => !currentValues.some((cv) => cv === v) ); const combined = [...new Set([...currentValues, ...newValues])]; textarea.value = combined.map(escapeCommas).join(","); } renderPills(); }; const createAddInput = () => { const addInput = document.createElement("input"); addInput.type = "text"; addInput.placeholder = "+ Add"; addInput.className = "bg-transparent focus:outline-none text-sm p-1 w-20 animate-fade-in"; addInput.onclick = (e) => e.stopPropagation(); addInput.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); addValues(addInput.value); } }; addInput.onpaste = (e) => { e.preventDefault(); const pasteData = (e.clipboardData || window.clipboardData).getData( "text" ); addValues(pasteData); }; return addInput; }; renderPills = () => { let values = textarea.value.trim() ? splitByUnescapedCommas(textarea.value) : []; const uniqueValues = [...new Set(values)]; const itemsToSort = uniqueValues.map((value) => ({ original: value, normalized: value.toUpperCase(), })); itemsToSort.sort((a, b) => a.normalized.localeCompare(b.normalized)); values = itemsToSort.map((item) => item.original); textarea.value = values.map(escapeCommas).join(","); pillsContainer.innerHTML = ""; let firstNewPill = null; values.forEach((value) => { const pill = createPill(value); pillsContainer.appendChild(pill); if (lastAdded.some((added) => added === value) && !firstNewPill) { firstNewPill = pill; } }); if (firstNewPill) firstNewPill.querySelector("span").focus(); lastAdded = []; const addInput = createAddInput(); pillsContainer.appendChild(addInput); textarea.classList.add("hidden"); pillsContainer.classList.remove("hidden"); }; pillsContainer.addEventListener("click", toTextareaView); textarea.addEventListener("blur", renderPills); textarea.addEventListener("renderpills", renderPills); textarea.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); const beforeValues = textarea.value.trim() ? splitByUnescapedCommas(textarea.value).map((v) => v.trim()) : []; renderPills(); const afterValues = textarea.value.trim() ? splitByUnescapedCommas(textarea.value).map((v) => v.trim()) : []; lastAdded = afterValues.filter((v) => !beforeValues.includes(v)); } }); renderPills(); }); } const modalHTML = ` <div id="medium-modal" class="fixed inset-0 z-1031 hidden"> <div class="min-h-screen w-full flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" data-modal-hide> <div class="w-full max-w-2xl bg-slate-100 rounded-2xl shadow-2xl flex flex-col max-h-[77vh] overflow-hidden border border-slate-200/80"> <div class="relative p-5 border-b border-slate-200/80"> <h3 class="text-lg font-semibold text-slate-800 text-center">Enhanced Filter Settings</h3> <div class="absolute top-3.5 right-3.5 flex gap-1"> <button type="button" class="p-2 text-slate-500 hover:text-[#a6004c] transition-colors rounded-full hover:bg-slate-200/70" data-modal-hide aria-label="Minimize Filter Settings"> ${minimizeSVG} </button> <button type="button" class="p-2 text-slate-500 hover:text-[#a6004c] transition-colors rounded-full hover:bg-slate-200/70" data-discard-filters data-modal-hide aria-label="Discard Changes"> ${cancelSVG} </button> </div> </div> <div class="flex-1 overflow-y-auto p-4 bg-slate-50"> <div class="space-y-4"> ${createFilterSection( "includeTags", "Include Games with Tags", "Only show games that have at least one of these tags.", "Examples: PC, GAME PASS, XSX Optimized, etc." )} ${createFilterSection( "excludeTags", "Exclude Games with Tags", "Hide games that have any of these tags.", "Examples: Preorder, EA, GAME PASS, etc" )} ${createFilterSection( "hiddenGames", "Hidden Games", "Games that will remain hidden from view.", "Examples: EA SPORTS, F1, NBA, etc." )} </div> </div> <div class="flex justify-center gap-3 p-4 bg-slate-100 border-t border-slate-200/80"> <button id="cancel-filters" type="button" data-discard-filters data-modal-hide class="px-5 py-2.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 hover:bg-slate-100 focus:ring-4 focus:outline-none focus:ring-slate-200 rounded-lg transition-colors"> Discard </button> <button id="apply-filters" type="button" class="px-5 py-2.5 text-sm font-medium text-white! bg-[#a6004c] hover:bg-[#8d0040] focus:ring-4 focus:outline-none focus:ring-[#a6004c]/50 rounded-lg transition-colors"> Apply Filters </button> </div> </div> </div> </div>`; document.body.insertAdjacentHTML("beforeend", modalHTML); initializePillInputs(); const splitByUnescapedCommas = (str) => { if (!str) return []; return str .split(/(?<!\\),/) .map((s) => s.trim()) .filter((s) => s.length > 0) .map((s) => s.replace(/\\,/g, ",")); }; const escapeCommas = (str) => str.replace(/,/g, "\\,"); function restoreInitialFilters() { let initialFilters = JSON.parse(JSON.stringify(filters)); ["includeTags", "excludeTags", "hiddenGames"].forEach((id) => { const filter = initialFilters[id]; document.getElementById(`${id}-enabled`).checked = filter.enabled; const textarea = document.getElementById(`${id}-values`); const values = filter.tags || filter.games || []; textarea.value = values.map(escapeCommas).join(","); textarea.dispatchEvent(new CustomEvent("renderpills")); }); } document.querySelectorAll("[data-modal-hide]").forEach((element) => { element.addEventListener("click", (e) => { if ( e.target === element || e.target.tagName === "svg" || e.target.tagName === "path" ) { e.preventDefault(); document.getElementById("medium-modal").classList.add("hidden"); } }); }); document.querySelectorAll("[data-discard-filters]").forEach((element) => { element.addEventListener("click", (e) => { if ( e.target === element || e.target.tagName === "svg" || e.target.tagName === "path" ) { e.preventDefault(); restoreInitialFilters(); } }); }); document.getElementById("apply-filters")?.addEventListener("click", () => { ["includeTags", "excludeTags", "hiddenGames"].forEach((id) => { const enabled = document.getElementById(`${id}-enabled`).checked; const textarea = document.getElementById(`${id}-values`); const values = splitByUnescapedCommas(textarea.value); filters[id] = { enabled, [id === "hiddenGames" ? "games" : "tags"]: values, }; }); localStorage.setItem("filters", JSON.stringify(filters)); document.getElementById("medium-modal").classList.add("hidden"); filterGames(); }); filterGames(); const filterButton = document.querySelector('a[href="#filterCollapse"]'); if (!filterButton) return; const originalRow = filterButton.closest(".row"); if (!originalRow) return; const originalText = filterButton.querySelector("span").textContent.trim(); const col = originalRow.querySelector(".col-md-6"); if (col) col.classList.replace("col-md-6", "col-md-3"); const newRow = document.createElement("div"); newRow.className = "col-md-3 col-xs-12 input-group px-[15px]! pb-[15px]!"; const icon = document.createElement("div"); icon.className = "input-group-addon w-[38px]!"; icon.innerHTML = filterSVG; icon.firstElementChild.setAttribute("class", "w-full h-full"); const link = document.createElement("a"); link.style.cssText = "padding-bottom: 5px; text-align: left; cursor: pointer;"; link.className = "btn btn-white btn-block btn-flat"; link.innerHTML = `<span>${originalText} +</span>`; link.addEventListener("click", () => { const modal = document.getElementById("medium-modal"); ["includeTags", "excludeTags", "hiddenGames"].forEach((id) => { const filter = filters[id]; const values = id === "hiddenGames" ? filter.games : filter.tags; const textarea = document.getElementById(`${id}-values`); if (textarea) { textarea.value = values.map(escapeCommas).join(","); textarea.dispatchEvent(new CustomEvent("renderpills")); } }); modal.classList.remove("hidden"); }); newRow.appendChild(icon); newRow.appendChild(link); originalRow.appendChild(newRow); })();