Greasy Fork

Greasy Fork is available in English.

MouseHunt - Marketplace UI Tweaks

Adds useful features and tweaks to the Marketplace rework

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MouseHunt - Marketplace UI Tweaks
// @author       Tran Situ (tsitu)
// @namespace    http://greasyfork.icu/en/users/232363-tsitu
// @version      1.6.2
// @description  Adds useful features and tweaks to the Marketplace rework
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==

(function () {
  /**
   * [ Notes ]
   * innerText has poor retrieval perf, use textContent
   *   http://perfectionkills.com/the-poor-misunderstood-innerText/
   * Is there a better way to center scrollRow vertically within table?
   *   https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
   */

  MutationObserver =
    window.MutationObserver ||
    window.WebKitMutationObserver ||
    window.MozMutationObserver;

  // Initialize 'Browse' tab item caching
  if (localStorage.getItem("marketplace-browse-cache-tsitu") === null) {
    const cacheObj = {
      "Cheese": 0,
      "Baskets & Kits": 0,
      "Charms": 0,
      "Crafting": 0,
      "Special": 0,
      "Collectibles": 0,
      "Weapons": 0,
      "Skins": 0
    };
    localStorage.setItem(
      "marketplace-browse-cache-tsitu",
      JSON.stringify(cacheObj)
    );
  }

  // Only observe changes to the #overlayPopup element
  const observerTarget = document.querySelector("#overlayPopup");

  const observer = new MutationObserver(function () {
    // Check if the Marketplace interface is open
    if (observerTarget.querySelector(".marketplaceView")) {
      // Disconnect and reconnect later to prevent mutation loop
      observer.disconnect();

      // Feature: Move close button to top right and clean up visuals
      const oldClose = observerTarget.querySelector(
        ".button[type=submit][value=Close]"
      );
      if (oldClose) {
        const newClose = oldClose.cloneNode();
        oldClose.remove();
        const suffix = observerTarget.querySelector(".suffix");
        if (suffix) suffix.remove();

        newClose.style.position = "absolute";
        newClose.style.right = "0px";
        newClose.style.top = "5px";
        observerTarget.querySelector(".marketplaceView").prepend(newClose);

        const searchContainer = observerTarget.querySelector(
          ".marketplaceView-header-searchContainer"
        );
        if (searchContainer) {
          searchContainer.style.right = "65px";
          searchContainer.style.width = "220px";
        }

        const searchBar = observerTarget.querySelector(
          ".marketplaceView-header-search"
        );
        if (searchBar) {
          searchBar.style.width = "184px";
        }

        // Remove 'X' in top right
        const topX = observerTarget.querySelector("#jsDialogClose");
        if (topX) topX.remove();
      }

      const browseTab = observerTarget.querySelector(
        "[data-tab=browse].active"
      );
      const backButton = observerTarget.querySelector(
        "a.marketplaceView-breadcrumb"
      );

      if (browseTab && !backButton) {
        /* Browse tab logic (active Browse tab + inactive 'Back' button) */

        // Align trend icon divs to the right
        const trendIcons = observerTarget.querySelectorAll(
          ".marketplaceView-trendIcon"
        );
        trendIcons.forEach(el => {
          const td = el.parentElement;
          if (td) {
            td.style.textAlign = "right";
          }
        });

        const sidebar = observerTarget.querySelector(
          ".marketplaceView-browse-sidebar"
        );
        const itemType = sidebar.querySelector(
          ".marketplaceView-browse-sidebar-link.active"
        );

        // Feature: Make item images 40x40 px
        observerTarget
          .querySelectorAll(".marketplaceView-itemImage")
          .forEach(el => {
            el.style.width = "40px";
            el.style.height = "40px";
            el.style.backgroundSize = "100%";
            el.style.minHeight = "40px";
          });

        let totalValueSum = 0;
        let totalValueSell = 0;
        /**
         * Abbreviates large number values up to 1 decimal point
         * k = 1,000 and m = 1,000,000
         * @param {number} num Integer to abbreviate
         * @return {string}
         */
        function abbrev(num) {
          if (num <= 999) {
            return "" + num;
          } else if (num >= 1000 && num <= 999999) {
            let pre = Math.floor(num / 1000);
            let post = Math.round((num % 1000) / 100);
            if (post === 10) {
              post = 0;
              pre += 1;
            }
            return `${pre}.${post}k`;
          } else if (num >= 1000000) {
            let pre = Math.floor(num / 1000000);
            let post = Math.round((num % 1000000) / 100000);
            if (post === 10) {
              post = 0;
              pre += 1;
            }
            return `${pre}.${post}m`;
          }
        }

        const rows = observerTarget.querySelectorAll("tr[data-item-id]");
        if (rows.length > 0) {
          const avgPriceHeader = observerTarget.querySelector(
            "th.marketplaceView-table-averagePrice"
          );

          const valueHeader = document.createElement("th");
          valueHeader.innerText = "Value";
          valueHeader.className =
            "marketplaceView-table-estvalue marketplaceView-table-numeric sortable";

          // Custom "Value" column sort
          valueHeader.onclick = function () {
            if (!valueHeader.classList.contains("active")) {
              valueHeader.classList.add("active");
              observerTarget
                .querySelectorAll(".marketplaceView-table .sortable")
                .forEach(el => {
                  if (
                    !el.classList.contains("marketplaceView-table-estvalue") &&
                    el.classList.contains("active")
                  ) {
                    el.classList.toggle("active");
                  }
                });
            } else {
              valueHeader.classList.toggle("reverse");
            }

            const unsortedArr = [];
            observerTarget
              .querySelectorAll(".marketplaceView-table tr[data-item-id]")
              .forEach(el => {
                unsortedArr.push(el);
                el.remove();
              });

            const sortedArr = unsortedArr.sort((a, b) => {
              let aT = a.title || 0;
              let bT = b.title || 0;

              if (typeof aT === "string") {
                aT = parseInt(aT.split(" Gold")[0].replace(/,/g, ""));
              }

              if (typeof bT === "string") {
                bT = parseInt(bT.split(" Gold")[0].replace(/,/g, ""));
              }

              if (valueHeader.classList.contains("reverse")) {
                return aT - bT; // Low > High
              } else {
                return bT - aT; // High > Low
              }
            });

            const targetTable = observerTarget.querySelector(
              ".marketplaceView-table tbody"
            );
            sortedArr.forEach(el => {
              targetTable.appendChild(el);
            });

            const emptyEl = observerTarget.querySelector(
              ".marketplaceView-table tr.empty"
            );
            if (emptyEl) {
              emptyEl.remove();
              targetTable.appendChild(emptyEl);
            }

            return false;
          };

          if (
            avgPriceHeader &&
            !observerTarget.querySelector(".marketplaceView-table-estvalue")
          ) {
            // Add 'Value' column header
            avgPriceHeader.insertAdjacentElement("afterend", valueHeader);

            rows.forEach(row => {
              // Add click handlers to the <a>'s that open up an item page
              row.querySelectorAll("a").forEach(el => {
                const aText = el.onclick;
                if (aText) {
                  if (aText.toString().indexOf("showItem") >= 0) {
                    el.addEventListener("click", function () {
                      // Parse current item name and type for caching
                      const name = row.querySelector(
                        ".marketplaceView-table-name"
                      );

                      if (name && itemType) {
                        // Retrieve and overwrite localStorage
                        const lsText = localStorage.getItem(
                          "marketplace-browse-cache-tsitu"
                        );
                        if (lsText) {
                          const lsObj = JSON.parse(lsText);
                          lsObj[itemType.textContent] = name.textContent;
                          localStorage.setItem(
                            "marketplace-browse-cache-tsitu",
                            JSON.stringify(lsObj)
                          );
                        }
                      }
                    });
                  }
                }
              });

              // Parse owned quantity
              let ownedNum = 0;
              const ownedText = row.querySelector(
                ".marketplaceView-table-quantity"
              ).textContent;
              if (ownedText !== "-") {
                ownedNum = parseInt(ownedText.split(",").join(""));
              }

              // Parse average prices
              let priceNum = 0;
              const priceText = row.querySelector(".marketplaceView-goldValue");
              if (priceText.children.length > 0) {
                priceNum = parseInt(
                  priceText.children[0].title.split(" ")[0].split(",").join("")
                );
              }

              const multValue = ownedNum * priceNum;
              if (multValue > 0) {
                totalValueSum += multValue;
                const sellValue = Math.floor((priceNum * 100) / 110) * ownedNum;
                totalValueSell += sellValue;
                row.title = `${multValue.toLocaleString()} Gold (Buy)\n${sellValue.toLocaleString()} Gold (Sell)`;
              }

              let outputText = abbrev(multValue);
              if (priceNum === 0) {
                // Avg. Price currently unavailable, but value isn't necessarily 0
                outputText = "N/A";
              }

              const valueColumn = document.createElement("td");
              valueColumn.innerText = outputText;
              valueColumn.className =
                "marketplaceView-table-numeric value-column-tsitu";

              // Feature: Insert 'Own' x 'Avg. Price' = 'Value' column data
              row
                .querySelector(".marketplaceView-table-averagePrice")
                .insertAdjacentElement("afterend", valueColumn);
            });
          }
        }

        // Add info to the sidebar
        if (
          sidebar &&
          !observerTarget.querySelector(".marketplace-sidebar-tsitu")
        ) {
          // Container div
          const div = document.createElement("div");
          div.className = "marketplace-sidebar-tsitu";
          div.style.margin = "20px";

          // Highlighted text
          const span1 = document.createElement("span");
          span1.style.backgroundColor = "#D6EBA1";
          span1.innerText = "highlighted green";

          // Other text
          const span2 = document.createElement("span");
          span2.innerText = "Last viewed item is ";

          div.appendChild(span2);
          div.appendChild(span1);

          // Feature: Add <span> with sum of values on current tab
          const filterDiv = observerTarget.querySelector(
            ".marketplaceView-browse-filterContainer"
          );
          if (
            filterDiv &&
            !observerTarget.querySelector(".marketplace-total-value-tsitu")
          ) {
            const span = document.createElement("span");
            span.className = "marketplace-total-value-tsitu";
            span.innerText = `Total estimated value on this tab: ${totalValueSum.toLocaleString()} (Buy)\n${totalValueSell.toLocaleString()} (Sell)`;

            div.appendChild(document.createElement("br"));
            div.appendChild(document.createElement("br"));
            div.appendChild(span);
          }

          // Inject into DOM
          sidebar.appendChild(div);
        }

        // Feature: Check cache for most recently clicked item and scroll to it
        const lsText = localStorage.getItem("marketplace-browse-cache-tsitu");
        if (lsText) {
          const lsObj = JSON.parse(lsText);
          const itemType = sidebar.querySelector(
            ".marketplaceView-browse-sidebar-link.active"
          );
          if (itemType) {
            const name = lsObj[itemType.textContent];
            if (name && name !== 0) {
              /**
               * Return row element that matches existing cached item name
               * @param {string} name Cached item name
               * @return {HTMLElement|false} <tr> that should be highlighted and scrolled to
               */
              function findElement(name) {
                for (let el of observerTarget.querySelectorAll(
                  "tr[data-item-id]"
                )) {
                  const aTags = el.querySelectorAll("a");
                  if (aTags.length === 5) {
                    if (name === aTags[2].textContent) {
                      return el;
                    }
                  }
                }

                return false;
              }

              // Calculate index for nth-child
              const targetEl = findElement(name);
              let nthChildValue = 0;
              for (let i = 0; i < rows.length; i++) {
                const el = rows[i];
                if (el === targetEl) {
                  nthChildValue = i + 2;
                  break;
                }
              }

              // tr:nth-child value (min = 2)
              const recentRow = observerTarget.querySelector(
                `.marketplaceView-table tr:nth-child(${nthChildValue})`
              );
              if (recentRow) {
                recentRow.style.backgroundColor = "#D6EBA1";

                // Try scrolling up to 4 rows down
                let scrollRow = recentRow;
                for (let i = 4; i > 0; i--) {
                  const row = observerTarget.querySelector(
                    `.marketplaceView-table tr:nth-child(${nthChildValue + i})`
                  );
                  if (row) {
                    scrollRow = row;
                    break;
                  }
                }

                scrollRow.scrollIntoView({
                  // Seems to wait for XHR & render - slow initially but gets moderately faster
                  behavior: "auto",
                  block: "nearest",
                  inline: "nearest"
                });
              }
            }
          }
        }
      } else if (backButton) {
        /* Listing logic (active 'Back' button) */

        // Feature: Inject tariff info into Sell & Buy Orders rows
        const sellOrders = observerTarget.querySelector(
          ".marketplaceView-item-quickListings.sell"
        );

        const buyOrders = observerTarget.querySelector(
          ".marketplaceView-item-quickListings.buy"
        );

        if (sellOrders && buyOrders) {
          const goldValues = observerTarget.querySelectorAll(
            "td .marketplaceView-goldValue:not(.marketplaceView-suggestedPrice):not(.tsitu-link-bo-minus):not(.tsitu-link-bo-plus)"
          );

          if (goldValues.length > 0) {
            goldValues.forEach(el => {
              const rawVal = el.textContent;
              if (typeof rawVal === "string") {
                const value = parseInt(rawVal.split(",").join(""));
                const tax = Math.ceil(value / 11);
                const preTax = value - tax;
                const titleString = `${preTax.toLocaleString()} (Raw)\n${tax.toLocaleString()} (Tax)`;

                const ppEl = el.parentElement.parentElement;
                if (ppEl.nodeName === "TR") ppEl.title = titleString;
                const qtyEl = ppEl.querySelector(
                  ".marketplaceView-table-listing-quantity"
                );

                // Add a reversible onclick with same info as title
                if (qtyEl && !qtyEl.onclick) {
                  const qtyVal = qtyEl.textContent;

                  function initHandler() {
                    qtyEl.innerText = qtyVal + "\n" + titleString;
                    qtyEl.onclick = revertHandler;
                  }

                  function revertHandler() {
                    qtyEl.innerText = qtyVal;
                    qtyEl.onclick = initHandler;
                  }

                  qtyEl.onclick = initHandler;
                }
              }
            });
          }
        }

        // Feature: More Quick Links
        const orderButton = observerTarget.querySelector(
          ".mousehuntActionButton.marketplaceView-item-submitButton"
        );
        if (orderButton) {
          // Price suggestion parent divs
          const qtySuggest = observerTarget.querySelector(
            ".marketplaceView-item-input.quantity .marketplaceView-item-input-suggested"
          );

          const txType = observerTarget.querySelector(
            ".marketplaceView-item-actionType .marketplaceView-listingType"
          ).classList[1];

          // Check existence of qtySuggest b/c directly clicking 'Sell' results in separate mutations
          if (txType === "sell" && qtySuggest) {
            // Existing 'Sell All' link to clone
            const sellAll = qtySuggest.children[0];

            // Check custom class name to prevent multiple appends
            if (sellAll && !observerTarget.querySelector(".tsitu-link-sabo")) {
              const saQty = parseInt(
                sellAll.textContent.split(": ")[1].split(",").join("")
              );
              if (saQty > 1) {
                const sellAllButOne = sellAll.cloneNode();
                sellAllButOne.className = "tsitu-link-sabo";
                sellAllButOne.setAttribute(
                  "onclick",
                  `hg.views.MarketplaceView.setOrderQuantity(${
                    saQty - 1
                  }); return false;`
                );
                sellAllButOne.innerText = `[ Sell All But One: ${(
                  saQty - 1
                ).toLocaleString()} ]`;
                qtySuggest.appendChild(document.createElement("br"));
                qtySuggest.appendChild(document.createElement("br"));
                qtySuggest.appendChild(sellAllButOne);
              }
            }

            const firstRow = sellOrders.querySelector(
              "td .marketplaceView-goldValue"
            );
            if (firstRow) {
              const rawVal = firstRow.textContent;
              if (typeof rawVal === "string") {
                const value = parseInt(rawVal.split(",").join(""));
                let offerValue = Math.round(value * 0.9999);
                if (offerValue >= value) offerValue = value - 1; // Minimum increment
                if (offerValue <= 10) offerValue = undefined; // Must be at least 10 Gold

                const bestSell = observerTarget.querySelector(
                  ".sell > .marketplaceView-bestPrice"
                );
                if (bestSell.textContent.length > 6) {
                  bestSell.innerText = bestSell.textContent.replace(
                    "Best",
                    "Quick Sell"
                  );
                }

                // Generate <a> manually, not guaranteed an existing link
                if (offerValue) {
                  const boMinusLink = document.createElement("a");
                  boMinusLink.href = "#";
                  boMinusLink.setAttribute(
                    "onclick",
                    `hg.views.MarketplaceView.setOrderPrice(${offerValue}); return false;`
                  );
                  boMinusLink.className =
                    "marketplaceView-goldValue tsitu-link-bo-minus";
                  boMinusLink.innerText = `[ Undercut - 0.01%: ${offerValue.toLocaleString()} ]`;
                  if (!observerTarget.querySelector(".tsitu-link-bo-minus")) {
                    if (bestSell) {
                      bestSell.insertAdjacentElement("afterend", boMinusLink);
                    }
                  }
                }
              }
            }
          } else if (txType === "buy") {
            const firstRow = buyOrders.querySelector(
              "td .marketplaceView-goldValue"
            );
            if (firstRow) {
              const rawVal = firstRow.textContent;
              if (typeof rawVal === "string") {
                const value = parseInt(rawVal.split(",").join(""));
                let offerValue = Math.round(value * 1.0001);
                if (offerValue <= value) offerValue = value + 1; // Minimum increment
                if (offerValue >= 4294967293) offerValue = undefined; // Maximum tx amount

                const bestBuy = observerTarget.querySelector(
                  ".buy > .marketplaceView-bestPrice"
                );
                if (bestBuy.textContent.length > 6) {
                  bestBuy.innerText = bestBuy.textContent.replace(
                    "Best",
                    "Quick Buy"
                  );
                }

                // Generate <a> manually, not guaranteed an existing link
                if (offerValue) {
                  const boPlusLink = document.createElement("a");
                  boPlusLink.href = "#";
                  boPlusLink.setAttribute(
                    "onclick",
                    `hg.views.MarketplaceView.setOrderPrice(${offerValue}); return false;`
                  );
                  boPlusLink.className =
                    "marketplaceView-goldValue tsitu-link-bo-plus";
                  boPlusLink.innerText = `[ Overbid + 0.01%: ${offerValue.toLocaleString()} ]`;
                  if (!observerTarget.querySelector(".tsitu-link-bo-plus")) {
                    if (bestBuy) {
                      bestBuy.insertAdjacentElement("afterend", boPlusLink);
                    }
                  }
                }
              }
            }
          }
        }

        // 'Back' scrolls too far but clicking 'Browse' tab works fine
        // Additional scrolling logic in showItemInBrowser() is to blame
        // Solution: Override 'Back' button behavior only on the 'Browse' tab
        if (browseTab) {
          backButton.setAttribute(
            "onclick",
            "hg.views.MarketplaceView.showBrowser(); return false;"
          );
        }
      }

      // Re-observe after mutation-inducing logic
      observer.observe(observerTarget, {
        childList: true,
        subtree: true
      });
    }
  });

  // Initial observe
  observer.observe(observerTarget, {
    childList: true,
    subtree: true
  });

  /**
   * 2020.09.28
   * loadMoreMyHistory and loadMoreMyHistoryByItem need a "self." in front of their showMyHistory calls
   * Simplest remedy for now seems to be force adding to global scope :/
   */
  const loadHistory = hg.views.MarketplaceView.loadMoreMyHistory.toString();
  if (loadHistory && loadHistory.indexOf("{showMyHistory") >= 0) {
    unsafeWindow.showMyHistory = function () {
      hg.views.MarketplaceView.showMyHistory();
    };
  }

  const loadItemHistory = hg.views.MarketplaceView.loadMoreMyHistoryByItem.toString();
  if (loadItemHistory && loadItemHistory.indexOf("{showMyHistoryByItem") >= 0) {
    unsafeWindow.showMyHistoryByItem = function (itemId) {
      hg.views.MarketplaceView.showMyHistoryByItem(itemId);
    };
  }
})();