您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enables sorting by price (ascending and descending) via the dropdown on a GOG wishlist page. Switching between "sort by price" and a native sorting option (title, date added, user reviews) automatically refreshes the page twice.
当前为
- // ==UserScript==
- // @name GOG Wishlist - Sort by Price (Dropdown)
- // @namespace https://github.com/idkicarus
- // @homepageURL https://github.com/idkicarus/GOG-wishlist-sort
- // @supportURL https://github.com/idkicarus/GOG-wishlist-sort/issues
- // @description Enables sorting by price (ascending and descending) via the dropdown on a GOG wishlist page. Switching between "sort by price" and a native sorting option (title, date added, user reviews) automatically refreshes the page twice.
- // @version 1.04
- // @license MIT
- // @match https://www.gog.com/account/wishlist*
- // @match https://www.gog.com/*/account/wishlist*
- // @run-at document-start
- // @grant none
- // ==/UserScript==
- (function () {
- // Global flags to track the sorting state.
- // lastSortWasPrice: Tracks if the last sort action was by price.
- // ascendingOrder: Determines if the current sort order should be ascending.
- let lastSortWasPrice = false;
- let ascendingOrder = true;
- // Retrieve the current refresh stage from sessionStorage.
- // This is used to coordinate a two-step refresh process that helps to avoid visual glitches.
- let refreshStage = sessionStorage.getItem("gog_sort_fix_stage");
- /**
- * Hides the wishlist section.
- *
- * Sets the opacity to 0 and disables pointer events so that the user does not see the unsorted list during refresh.
- */
- function hideWishlist() {
- let wishlistSection = document.querySelector(".account__product-lists");
- if (wishlistSection) {
- wishlistSection.style.opacity = "0";
- wishlistSection.style.pointerEvents = "none";
- }
- }
- /**
- * Shows the wishlist section.
- *
- * Restores the opacity and re-enables pointer events to reveal the sorted content.
- */
- function showWishlist() {
- let wishlistSection = document.querySelector(".account__product-lists");
- if (wishlistSection) {
- wishlistSection.style.opacity = "1";
- wishlistSection.style.pointerEvents = "auto";
- }
- }
- // If we are in the first stage of refresh (refreshStage equals "1"),
- // wait until the DOM is fully loaded and then hide the wishlist.
- // This prevents the user from seeing the intermediate unsorted state.
- if (refreshStage === "1") {
- document.addEventListener("DOMContentLoaded", () => {
- hideWishlist();
- });
- }
- /**
- * Sorts the wishlist products by price.
- *
- * This function performs the following steps:
- * 1. Logs the start of the sort process.
- * 2. Retrieves the wishlist container (the second element with class 'list-inner').
- * 3. Collects all product rows from the container.
- * 4. Iterates over each product row to extract the product title and price.
- * - Separates items with a valid price from those marked as "TBA" (to be announced).
- * 5. Determines the sort order:
- * - If the last sort was by price, toggle the order (ascending/descending);
- * - Otherwise, default to ascending.
- * 6. Sorts the priced items based on the selected order.
- * 7. Clears the current product rows from the container.
- * 8. Appends the sorted priced items followed by TBA items back to the container.
- * 9. Sets the flag indicating that the last sort action was by price.
- */
- function sortByPrice() {
- console.log("[Sort By Price] Sorting Started.");
- // Retrieve the wishlist container element (using the second occurrence of '.list-inner').
- let listInner = document.querySelectorAll('.list-inner')[1];
- if (!listInner) {
- console.error("[Sort By Price] ERROR: Wishlist list-inner element not found.");
- return;
- }
- // Convert the NodeList of product rows to an array.
- let productRows = Array.from(listInner.querySelectorAll('.product-row-wrapper'));
- console.log(`[Sort By Price] Found ${productRows.length} product rows.`);
- let pricedItems = []; // Array to hold products with valid prices.
- let tbaItems = []; // Array to hold products that are TBA or marked as SOON.
- // Process each product row.
- productRows.forEach(row => {
- // Extract the title from the product row.
- const titleElement = row.querySelector('.product-row__title');
- const title = titleElement ? titleElement.innerText.trim() : "Unknown Title";
- // Extract the standard price and discounted price elements.
- const priceElement = row.querySelector('._price.product-state__price');
- const discountElement = row.querySelector('.price-text--discount span.ng-binding');
- // Check if the game is flagged as "SOON" by inspecting a dedicated element.
- const soonFlag = row.querySelector('.product-title__flag--soon');
- // Determine the price: if a discount price exists, use it; otherwise, use the standard price.
- let priceText = discountElement ? discountElement.innerText : priceElement ? priceElement.innerText : null;
- // Convert the price text to a numeric value by stripping out non-numeric characters.
- let priceNumeric = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '').replace(/,/g, '')) : null;
- // Check if the product is marked as TBA by determining the visibility of the TBA badge.
- const tbaBadge = row.querySelector('.product-state__is-tba');
- const isTbaVisible = tbaBadge && tbaBadge.offsetParent !== null;
- // Use the visibility check or missing price to classify as TBA.
- const isTBA = isTbaVisible || priceText === null;
- // If the item is TBA, or its price is set to 99.99 with a "SOON" flag, or the price is not a number,
- // add it to the TBA list; otherwise, add it to the priced items list.
- if (isTBA || (priceNumeric === 99.99 && soonFlag) || isNaN(priceNumeric)) {
- console.log(`[Sort By Price] Marked as TBA/SOON: ${title}`);
- tbaItems.push(row);
- } else {
- console.log(`[Sort By Price] ${title} - Extracted Price: ${priceNumeric}`);
- pricedItems.push({ row, price: priceNumeric, title });
- }
- });
- // Determine sort order:
- // If the last sort was by price, toggle the order; if not, default to ascending.
- ascendingOrder = lastSortWasPrice ? !ascendingOrder : true;
- // Sort the priced items based on price.
- pricedItems.sort((a, b) => (ascendingOrder ? a.price - b.price : b.price - a.price));
- // Force a reflow by briefly hiding and showing the container.
- listInner.style.display = "none";
- listInner.offsetHeight;
- listInner.style.display = "block";
- // Clear current content of the wishlist container.
- while (listInner.firstChild) {
- listInner.removeChild(listInner.firstChild);
- }
- // Append sorted priced items first.
- pricedItems.forEach(item => listInner.appendChild(item.row));
- // Append TBA items after the priced items.
- tbaItems.forEach(item => listInner.appendChild(item));
- // Set flag indicating that the last sort action was by price.
- lastSortWasPrice = true;
- console.log("[Sort By Price] Sorting Completed.");
- }
- /**
- * Handles switching back to the native sort method.
- *
- * If the sort was changed after sorting by price, this function triggers a two-stage page refresh
- * to revert the changes smoothly.
- *
- * @param {string} option - The native sort option selected by the user.
- */
- function handleNativeSortClick(option) {
- console.log(`[Sort By Price] Switching to native sort: ${option}`);
- // If we're in the second stage of refresh, remove the flag and show the wishlist.
- if (refreshStage === "1") {
- console.log("[Sort By Price] Second refresh triggered to apply sorting.");
- sessionStorage.removeItem("gog_sort_fix_stage");
- showWishlist();
- return;
- }
- // Otherwise, set the refresh stage and hide the wishlist before reloading.
- sessionStorage.setItem("gog_sort_fix_stage", "1");
- console.log("[Sort By Price] First refresh (hiding only wishlist section).");
- hideWishlist();
- setTimeout(() => {
- location.reload();
- }, 50);
- }
- /**
- * Adds the "Price" sort option to the existing dropdown menu.
- *
- * This function:
- * 1. Searches for the dropdown container.
- * 2. If not found, retries after a short delay.
- * 3. Creates a new span element representing the "Price" option.
- * 4. Attaches an event listener to handle sorting when clicked.
- * 5. Appends the new option to the dropdown.
- * 6. Adds event listeners to the native sort options to handle switching back if needed.
- */
- function addSortByPriceOption() {
- const dropdown = document.querySelector(".header__dropdown ._dropdown__items");
- if (!dropdown) {
- console.log("[Sort By Price] WARNING: Dropdown not found. Retrying...");
- // If the dropdown is not found, try again after 500ms (wait for DOM elements to be available)
- setTimeout(addSortByPriceOption, 500);
- return;
- }
- // If the "Price" sort option has already been added, exit early
- if (document.querySelector("#sort-by-price")) return;
- // Create a new span element to serve as the "Price" sort option
- let sortPriceOption = document.createElement("span");
- sortPriceOption.id = "sort-by-price";
- sortPriceOption.className = "_dropdown__item";
- sortPriceOption.innerText = "Price";
- // When the "Price" option is clicked, sort the wishlist and update the header.
- sortPriceOption.addEventListener("click", () => {
- sortByPrice();
- updateSortHeader("Price");
- });
- // Add the new option to the dropdown.
- dropdown.appendChild(sortPriceOption);
- console.log("[Sort By Price] 'Price' option added to sort dropdown.");
- // Add click event listeners to all other native sort options in the dropdown.
- // When any of these are clicked after a "Price" sort, trigger the native sort refresh process.
- document.querySelectorAll(".header__dropdown ._dropdown__item").forEach(item => {
- if (item.id !== "sort-by-price") {
- item.addEventListener("click", () => {
- // Only trigger the native sort refresh if the last sort was by price
- if (lastSortWasPrice) {
- handleNativeSortClick(item.innerText);
- }
- });
- }
- });
- }
- /**
- * Updates the sort header in the dropdown to reflect the current sorting option.
- *
- * This function:
- * 1. Finds the header element.
- * 2. Hides any native sort option indicators.
- * 3. Creates or updates a custom header element with the provided sort option text.
- *
- * @param {string} option - The sort option to display (e.g., "Price").
- */
- function updateSortHeader(option) {
- console.log(`[Sort By Price] Updating sort header to: ${option}`);
- const sortHeader = document.querySelector(".header__dropdown ._dropdown__pointer-wrapper");
- if (!sortHeader) {
- console.log("[Sort By Price] ERROR: Sort header not found.");
- return;
- }
- // Hide any elements that show native sort options.
- document.querySelectorAll(".header__dropdown span[ng-show]").forEach(el => {
- el.style.display = "none";
- });
- // Check if the custom sort header exists; if not, create it.
- let customSortHeader = document.querySelector("#sort-by-price-header");
- if (!customSortHeader) {
- customSortHeader = document.createElement("span");
- customSortHeader.id = "sort-by-price-header";
- customSortHeader.className = "";
- sortHeader.insertBefore(customSortHeader, sortHeader.firstChild);
- }
- // Update the custom header text and make it visible.
- customSortHeader.innerText = option;
- customSortHeader.style.display = "inline-block";
- }
- // Create a MutationObserver to watch for the dropdown menu element.
- // When the dropdown is found, add the "Price" sort option and disconnect the observer.
- const observer = new MutationObserver((mutations, obs) => {
- if (document.querySelector(".header__dropdown ._dropdown__items")) {
- addSortByPriceOption();
- obs.disconnect();
- }
- });
- observer.observe(document.body, { childList: true, subtree: true });
- // If we are in the refresh stage "1", trigger a second refresh after a short delay.
- if (refreshStage === "1") {
- console.log("[Sort By Price] Performing second refresh to finalize sorting.");
- sessionStorage.removeItem("gog_sort_fix_stage");
- setTimeout(() => {
- location.reload();
- }, 50);
- }
- })();