GOG Wishlist - Sort by Price (Button)

Enables sorting by price (ascending and descending) via a button 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.

  1. // ==UserScript==
  2. // @name GOG Wishlist - Sort by Price (Button)
  3. // @namespace https://github.com/idkicarus
  4. // @homepageURL https://github.com/idkicarus/GOG-wishlist-sort
  5. // @supportURL https://github.com/idkicarus/GOG-wishlist-sort/issues
  6. // @description Enables sorting by price (ascending and descending) via a button 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.
  7. // @version 1.04
  8. // @license MIT
  9. // @match https://www.gog.com/account/wishlist*
  10. // @match https://www.gog.com/*/account/wishlist*
  11. // @run-at document-end
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. // --------------------------------------------------
  17. // Global State Variables
  18. // --------------------------------------------------
  19. // `ascendingOrder`: Boolean flag that determines if sorting is ascending.
  20. // `lastSortWasPrice`: Tracks whether the last sort action was performed using the custom price sort.
  21. // `refreshStage`: A flag stored in sessionStorage to manage a two-stage refresh process when switching back to native sorting.
  22. let ascendingOrder = true;
  23. let lastSortWasPrice = false;
  24. let refreshStage = sessionStorage.getItem("gog_sort_fix_stage");
  25.  
  26. // --------------------------------------------------
  27. // Helper Functions for Managing Wishlist Visibility
  28. // --------------------------------------------------
  29.  
  30. /**
  31. * hideWishlist
  32. *
  33. * Hides the wishlist section by reducing its opacity and disabling pointer events.
  34. * This is used during the refresh process to prevent users from seeing a transitional or unsorted state.
  35. */
  36. function hideWishlist() {
  37. const wishlistSection = document.querySelector(".account__product-lists");
  38. if (wishlistSection) {
  39. wishlistSection.style.opacity = "0";
  40. wishlistSection.style.pointerEvents = "none";
  41. }
  42. }
  43.  
  44. /**
  45. * showWishlist
  46. *
  47. * Restores the wishlist section's visibility and re-enables pointer events.
  48. * This is called after the refresh process to reveal the sorted or native list.
  49. */
  50. function showWishlist() {
  51. const wishlistSection = document.querySelector(".account__product-lists");
  52. if (wishlistSection) {
  53. wishlistSection.style.opacity = "1";
  54. wishlistSection.style.pointerEvents = "auto";
  55. }
  56. }
  57.  
  58. // --------------------------------------------------
  59. // Two-Stage Refresh for Native Sorting
  60. // --------------------------------------------------
  61. // When switching back to a native sort, a two-stage refresh process is used:
  62. // 1. The first stage hides the wishlist to avoid showing a transitional state.
  63. // 2. A second refresh reloads the page and then reveals the wishlist.
  64. if (refreshStage === "1") {
  65. // Once the DOM content is loaded, hide the wishlist.
  66. document.addEventListener("DOMContentLoaded", () => {
  67. hideWishlist();
  68. });
  69. console.log("[Sort By Price] In refresh stage 1; scheduling second refresh.");
  70. // Schedule the second refresh after a short delay.
  71. setTimeout(() => {
  72. console.log("[Sort By Price] Performing second refresh to finalize native sorting.");
  73. sessionStorage.removeItem("gog_sort_fix_stage"); // Clear the refresh flag.
  74. location.reload(); // Reload the page to complete native sorting.
  75. }, 50);
  76. }
  77.  
  78. // --------------------------------------------------
  79. // Custom Sorting Logic: "Sort by Price" Button
  80. // --------------------------------------------------
  81.  
  82. // Create a button element for sorting the wishlist by price.
  83. const sort_btn = document.createElement("button");
  84. sort_btn.innerHTML = "Sort by Price";
  85.  
  86. // Attach an event listener to the button to trigger sorting when clicked.
  87. sort_btn.addEventListener("click", () => {
  88. console.log("[Sort By Price] Button Clicked. Sorting Started.");
  89.  
  90. // Retrieve the wishlist container; it is assumed to be the second element with class 'list-inner'.
  91. const listInner = document.querySelectorAll('.list-inner')[1];
  92. if (!listInner) {
  93. console.error("[Sort By Price] ERROR: .list-inner element not found.");
  94. return;
  95. }
  96.  
  97. // Get all product rows within the wishlist.
  98. const productRows = Array.from(listInner.querySelectorAll('.product-row-wrapper'));
  99. console.log(`[Sort By Price] Found ${productRows.length} product rows.`);
  100.  
  101. // Initialize arrays to store items with valid prices and those marked as TBA/SOON.
  102. const pricedItems = [];
  103. const tbaItems = [];
  104.  
  105. // Process each product row to extract title and price information.
  106. productRows.forEach(row => {
  107. // Retrieve the product title; default to "Unknown Title" if not found.
  108. const titleElement = row.querySelector('.product-row__title');
  109. const title = titleElement ? titleElement.innerText.trim() : "Unknown Title";
  110.  
  111. // Retrieve price elements: the standard price and any discount price.
  112. const priceElement = row.querySelector('._price.product-state__price');
  113. const discountElement = row.querySelector('.price-text--discount span.ng-binding');
  114.  
  115. // Check for a "SOON" flag that indicates the product is not yet available.
  116. const soonFlag = row.querySelector('.product-title__flag--soon');
  117.  
  118. // Determine which price text to use; prefer the discount price if available.
  119. const priceText = discountElement ? discountElement.innerText : priceElement ? priceElement.innerText : null;
  120. // Convert the extracted price text into a numeric value.
  121. const priceNumeric = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '').replace(/,/g, '')) : null;
  122.  
  123. // Check if a "TBA" badge is visibly displayed (i.e., its parent element is not hidden).
  124. const tbaBadge = row.querySelector('.product-state__is-tba');
  125. const isTbaVisible = tbaBadge && tbaBadge.offsetParent !== null;
  126.  
  127. // Determine if this product should be treated as TBA.
  128. const isTBA = isTbaVisible || priceText === null;
  129.  
  130. // Categorize the product:
  131. // - If it's marked as TBA (or its price is a placeholder like 99.99 with a "SOON" flag, or the price is not a number),
  132. // add it to the tbaItems array.
  133. // - Otherwise, add it to the pricedItems array with its associated data.
  134. if (isTBA || (priceNumeric === 99.99 && soonFlag) || isNaN(priceNumeric)) {
  135. console.log(`[Sort By Price] Marked as TBA/SOON: ${title}`);
  136. tbaItems.push(row);
  137. } else {
  138. console.log(`[Sort By Price] ${title} - Extracted Price: ${priceNumeric}`);
  139. pricedItems.push({ row, price: priceNumeric, title });
  140. }
  141. });
  142.  
  143. console.log("[Sort By Price] Sorting priced items...");
  144. // Sort the array of priced items in ascending or descending order based on the current flag.
  145. pricedItems.sort((a, b) => ascendingOrder ? a.price - b.price : b.price - a.price);
  146. console.log("[Sort By Price] Sorted Prices:", pricedItems.map(p => `${p.title}: $${p.price}`));
  147.  
  148. // Rearrange the wishlist: add the sorted priced items first, then append the TBA items in their original order.
  149. pricedItems.forEach(item => listInner.appendChild(item.row));
  150. tbaItems.forEach(item => listInner.appendChild(item));
  151.  
  152. // Toggle the sort order for the next click and record that the last sort action was by price.
  153. ascendingOrder = !ascendingOrder;
  154. lastSortWasPrice = true;
  155. console.log("[Sort By Price] Sorting Completed.");
  156. });
  157.  
  158. // --------------------------------------------------
  159. // Append the "Sort by Price" Button to the UI
  160. // --------------------------------------------------
  161. // The button is added to the page after a delay to ensure that the target container has loaded.
  162. if (/wishlist/.test(document.location.href)) {
  163. setTimeout(() => {
  164. let el;
  165. // Identify the appropriate header element to which the button should be appended.
  166. const header = document.querySelector(".header__main");
  167. if (/Wishlisted by/.test(header?.innerHTML)) {
  168. el = document.querySelector(".collection-header");
  169. } else {
  170. const headers = document.querySelectorAll(".header__main");
  171. el = headers[headers.length - 1];
  172. }
  173. if (el) {
  174. el.appendChild(sort_btn);
  175. console.log("[Sort By Price] Sort button added to UI.");
  176. }
  177. }, 900);
  178. }
  179.  
  180. // --------------------------------------------------
  181. // Native Sort Refresh Logic
  182. // --------------------------------------------------
  183.  
  184. /**
  185. * handleNativeSortClick
  186. *
  187. * Handles clicks on native sort options (such as sort by title, date added, or user reviews).
  188. * When the last sort was performed using the custom "Sort by Price" button,
  189. * this function triggers a two-stage refresh process:
  190. * 1. Set a flag in sessionStorage and reload the page with the wishlist hidden.
  191. * 2. After reload, clear the flag and show the wishlist to allow the native sort to take effect.
  192. *
  193. * @param {string} option - The label of the native sort option selected.
  194. */
  195. function handleNativeSortClick(option) {
  196. console.log(`[Sort By Price] Switching to native sort: ${option}`);
  197. // If already in the first refresh stage, complete the process.
  198. if (refreshStage === "1") {
  199. console.log("[Sort By Price] Second refresh triggered to apply native sorting.");
  200. sessionStorage.removeItem("gog_sort_fix_stage");
  201. showWishlist();
  202. return;
  203. }
  204. // Set the refresh flag for the first stage and hide the wishlist.
  205. sessionStorage.setItem("gog_sort_fix_stage", "1");
  206. console.log("[Sort By Price] First refresh (hiding wishlist before native sort).");
  207. hideWishlist();
  208. // Reload the page after a short delay to initiate native sorting.
  209. setTimeout(() => {
  210. location.reload();
  211. }, 50);
  212. }
  213.  
  214. /**
  215. * addNativeSortListeners
  216. *
  217. * Attaches event listeners to native sort dropdown items.
  218. * These listeners trigger the native sort refresh logic when a native sort option is clicked
  219. * after a custom price sort has been applied.
  220. */
  221. function addNativeSortListeners() {
  222. // Select all native sort dropdown items.
  223. const nativeSortItems = document.querySelectorAll(".header__dropdown ._dropdown__item");
  224. // If the native sort options are not yet available, retry after a short delay.
  225. if (!nativeSortItems.length) {
  226. setTimeout(addNativeSortListeners, 500);
  227. return;
  228. }
  229. // Attach click event listeners to each native sort item.
  230. nativeSortItems.forEach(item => {
  231. // Ensure that multiple listeners are not attached to the same element.
  232. if (!item.dataset.priceSortListenerAdded) {
  233. item.addEventListener("click", () => {
  234. // Only trigger the refresh process if the last sort was done by the custom price sort.
  235. if (lastSortWasPrice) {
  236. handleNativeSortClick(item.innerText);
  237. lastSortWasPrice = false; // Reset the flag after handling.
  238. }
  239. });
  240. // Mark this element as having a listener attached.
  241. item.dataset.priceSortListenerAdded = "true";
  242. }
  243. });
  244. }
  245.  
  246. // Use a MutationObserver to monitor the DOM for the insertion of the native sort dropdown.
  247. // Once the dropdown is found, attach native sort listeners.
  248. const observer = new MutationObserver((mutations, obs) => {
  249. if (document.querySelector(".header__dropdown ._dropdown__items")) {
  250. addNativeSortListeners();
  251. obs.disconnect(); // Stop observing once listeners are attached.
  252. }
  253. });
  254. // Start observing the document body for dynamic changes.
  255. observer.observe(document.body, { childList: true, subtree: true });
  256. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址