您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enables wishlist sorting by price (ascending and descending); switching between "sort by price" and a native sorting option (i.e., title, date added, user reviews), requires 2 page refreshes.
当前为
- // ==UserScript==
- // @name GOG Wishlist - Sort by Price (Dropdown)
- // @description Enables wishlist sorting by price (ascending and descending); switching between "sort by price" and a native sorting option (i.e., title, date added, user reviews), requires 2 page refreshes.
- // @version 1.0
- // @license MIT
- // @match https://www.gog.com/account/wishlist*
- // @match https://www.gog.com/*/account/wishlist*
- // @run-at document-start
- // @grant none
- // @namespace https://greasyfork.org/users/1435312
- // ==/UserScript==
- (function () {
- // Global flags to track whether the last sort was by price and the current sort order (ascending vs descending)
- let lastSortWasPrice = false;
- let ascendingOrder = true;
- // Retrieve the refresh stage from sessionStorage.
- // This is used to control a two-step refresh process which minimizes visual glitches when switching sort methods.
- let refreshStage = sessionStorage.getItem("gog_sort_fix_stage");
- /**
- * Hides the wishlist section by setting its opacity to 0 and disabling pointer events.
- * This method is used to prevent users from seeing the intermediate state during a refresh.
- */
- function hideWishlist() {
- // Select the wishlist container element
- let wishlistSection = document.querySelector(".account__product-lists");
- if (wishlistSection) {
- wishlistSection.style.opacity = "0"; // Make wishlist invisible but still present in the DOM
- wishlistSection.style.pointerEvents = "none"; // Disable interactions with the wishlist
- }
- }
- /**
- * Shows the wishlist section by restoring its opacity and pointer events.
- * This is called after the refresh process to reveal the sorted content.
- */
- function showWishlist() {
- let wishlistSection = document.querySelector(".account__product-lists");
- if (wishlistSection) {
- wishlistSection.style.opacity = "1"; // Restore visibility
- wishlistSection.style.pointerEvents = "auto"; // Re-enable interactions
- }
- }
- // If we are on the first refresh stage (i.e. refreshStage equals "1"),
- // wait for the DOM to be loaded and then hide the wishlist.
- // This ensures that during the refresh process, the user does not see an unsorted list.
- if (refreshStage === "1") {
- document.addEventListener("DOMContentLoaded", () => {
- hideWishlist();
- });
- }
- /**
- * Sorts the wishlist products by price.
- * This function:
- * - Locates the wishlist container element.
- * - Extracts product rows and separates them into priced items and TBA (to be announced) items.
- * - Determines the sort order (ascending or descending) based on whether the last sort was by price.
- * - Rebuilds the DOM with sorted priced items followed by TBA items.
- */
- function sortByPrice() {
- console.log("[Sort By Price] Sorting Started.");
- // Query all elements with class 'list-inner' and use the second one (index 1) which contains the wishlist
- let listInner = document.querySelectorAll('.list-inner')[1];
- if (!listInner) {
- console.error("[Sort By Price] ERROR: Wishlist list-inner element not found.");
- return;
- }
- // Get all wishlist product rows as an array
- let productRows = Array.from(listInner.querySelectorAll('.product-row-wrapper'));
- console.log(`[Sort By Price] Found ${productRows.length} product rows.`);
- // Separate items into those with a price (pricedItems) and those that are "TBA" or not priced (tbaItems)
- let pricedItems = [];
- let tbaItems = [];
- // Process each product row
- productRows.forEach(row => {
- // Attempt to find the standard price element and the discounted price element if available
- const priceElement = row.querySelector('._price.product-state__price');
- const discountElement = row.querySelector('.price-text--discount span.ng-binding');
- // Check if there is a flag indicating the product is "coming soon"
- const soonFlag = row.querySelector('.product-title__flag--soon');
- // Determine the price text: use discount text if available, otherwise standard price text
- let priceText = discountElement
- ? discountElement.innerText
- : priceElement
- ? priceElement.innerText
- : null;
- // Convert the extracted text into a numeric value by stripping non-numeric characters
- let priceNumeric = priceText
- ? parseFloat(priceText.replace(/[^0-9.]/g, '').replace(/,/g, ''))
- : null;
- // Create a text content check to mark items that are "TBA"
- // Also consider soonFlag and absence of priceText as indicators for TBA status
- const textContent = priceElement ? priceElement.textContent.toUpperCase() : "";
- const isTBA = textContent.includes("TBA") || soonFlag || priceText === null;
- // Special case: if the price is exactly 99.99 and the "soon" flag is present, treat it as TBA
- // because the 99.99 is a placeholder for the real price
- if (isTBA || (priceNumeric && priceNumeric === 99.99 && soonFlag)) {
- tbaItems.push(row);
- } else {
- // For items with valid prices, push an object with the row and its numeric price
- pricedItems.push({ row, price: priceNumeric });
- }
- });
- // Toggle sorting order:
- // If the last sorting operation was by price, switch the order (toggle ascending/descending).
- // Otherwise, default to ascending order.
- ascendingOrder = lastSortWasPrice ? !ascendingOrder : true;
- pricedItems.sort((a, b) => (ascendingOrder ? a.price - b.price : b.price - a.price));
- // Trigger a reflow by briefly hiding and showing the list container.
- listInner.style.display = "none";
- listInner.offsetHeight; // Force reflow
- listInner.style.display = "block";
- // Clear all current child elements from the list container.
- while (listInner.firstChild) {
- listInner.removeChild(listInner.firstChild);
- }
- // Append sorted priced items first.
- pricedItems.forEach(item => listInner.appendChild(item.row));
- // Append TBA items at the end in their original order.
- tbaItems.forEach(item => listInner.appendChild(item));
- // Set flag indicating that sorting was done by price
- lastSortWasPrice = true;
- console.log("[Sort By Price] Sorting Completed.");
- }
- /**
- * Handles clicks on native sort options.
- * When a native sort option is selected after a custom "Price" sort, a two-stage refresh is triggered:
- * - First refresh hides the wishlist.
- * - Second refresh restores visibility and applies the native sort.
- *
- * @param {string} option - The label of the native sort option clicked.
- */
- function handleNativeSortClick(option) {
- console.log(`[Sort By Price] Switching to native sort: ${option}`);
- // If we're in the middle of the refresh process (first refresh already occurred)
- // then trigger the second refresh.
- if (refreshStage === "1") {
- console.log("[Sort By Price] Second refresh triggered to apply sorting.");
- sessionStorage.removeItem("gog_sort_fix_stage"); // Clear the refresh stage flag
- showWishlist(); // Reveal the wishlist
- return;
- }
- // If this is the first time switching away from "Price" sort, set the refresh stage flag.
- sessionStorage.setItem("gog_sort_fix_stage", "1");
- console.log("[Sort By Price] First refresh (hiding only wishlist section).");
- hideWishlist(); // Hide the wishlist section before refresh
- setTimeout(() => {
- // Reload the page after a short delay to let the UI update
- location.reload();
- }, 50); // 50ms delay is used to ensure the UI hides before the reload occurs
- }
- /**
- * Adds a "Price" sorting option to the sort dropdown.
- * This function waits until the dropdown is available in the DOM and then adds:
- * - A new option to sort by price.
- * - Event listeners on native sort options to handle refresh if a previous "Price" sort was active.
- */
- function addSortByPriceOption() {
- // Find the dropdown container for sorting options
- 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 clicked, sort the wishlist by price and update the sort header text
- sortPriceOption.addEventListener("click", () => {
- sortByPrice();
- updateSortHeader("Price");
- });
- // Append the new sort option to the dropdown list
- 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 displayed in the UI to reflect the currently active sort option.
- *
- * @param {string} option - The label of the sort option to display.
- */
- function updateSortHeader(option) {
- console.log(`[Sort By Price] Updating sort header to: ${option}`);
- // Find the container for the sort header pointer
- const sortHeader = document.querySelector(".header__dropdown ._dropdown__pointer-wrapper");
- if (!sortHeader) {
- console.log("[Sort By Price] ERROR: Sort header not found.");
- return;
- }
- // Hide any existing sort labels that are controlled by Angular's ng-show directive
- document.querySelectorAll(".header__dropdown span[ng-show]").forEach(el => {
- el.style.display = "none";
- });
- // Look for a custom header element we may have already created for the "Price" sort
- let customSortHeader = document.querySelector("#sort-by-price-header");
- if (!customSortHeader) {
- // If not found, create one and insert it at the beginning of the sort header container
- customSortHeader = document.createElement("span");
- customSortHeader.id = "sort-by-price-header";
- customSortHeader.className = "";
- sortHeader.insertBefore(customSortHeader, sortHeader.firstChild);
- }
- // Update the header text and ensure it is visible
- customSortHeader.innerText = option;
- customSortHeader.style.display = "inline-block";
- }
- /**
- * A MutationObserver is set up to monitor the document for when the sort dropdown is added to the DOM.
- * Once detected, the "Price" sort option is added and the observer disconnects to prevent further calls.
- */
- const observer = new MutationObserver((mutations, obs) => {
- // Check if the dropdown container for sort options is present
- if (document.querySelector(".header__dropdown ._dropdown__items")) {
- addSortByPriceOption(); // Add the "Price" option to the dropdown
- obs.disconnect(); // Stop observing since our work is done
- }
- });
- // Begin observing the body for changes in child elements and subtree modifications
- observer.observe(document.body, { childList: true, subtree: true });
- /**
- * If the script detects that it is in the first refresh stage (refreshStage equals "1"),
- * perform a second refresh after a short delay. This ensures that any changes made during
- * the refresh process are fully applied.
- */
- if (refreshStage === "1") {
- console.log("[Sort By Price] Performing second refresh to finalize sorting.");
- sessionStorage.removeItem("gog_sort_fix_stage"); // Clear the refresh flag
- setTimeout(() => {
- // Reload the page after 50ms to allow any pending UI updates to complete
- location.reload();
- }, 50); // 50ms delay; honestly, could be longer to ensure no race conditions before reload
- }
- })();