Item Market Auto Price

Automatically set the price of items relative to the current market with settings menu

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Item Market Auto Price
// @namespace    dev.kwack.torn.imarket-auto-price
// @version      1.1.3
// @description  Automatically set the price of items relative to the current market with settings menu
// @author       Kwack (original), Mr_Awaken (modified)
// @match        https://www.torn.com/page.php?sid=ItemMarket
// @connect      api.torn.com
// @grant        GM_addStyle
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

// @ts-check

const inputSelector = `
  div[class*=itemRowWrapper] div[class*=priceInputWrapper]
  > div.input-money-group > input.input-money:not([type=hidden]):not(.kw--price-set),
  div[class*=itemRowWrapper] div[class*=amountInputWrapper][class*=hideMaxButton]
  > div.input-money-group > input.input-money:not([type=hidden]):not(.kw--price-set)
`;

const diff = 5;

let mainCalled = false;

// Function to show API key modal
function showApiKeyModal() {
  console.log("Showing API key modal");
  const modalHTML = `
    <div id="kw-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%;
         background: rgba(0, 0, 0, 0.8); z-index: 9999; display: flex; justify-content: center; align-items: center;">
      <div style="background: #1c1c1c; padding: 30px; border-radius: 8px; width: 400px; max-width: 90%;
          color: #fff; text-align: center; font-family: Arial, sans-serif;">
        <h2 style="margin-bottom: 10px;">🔐 Enter Your Torn API Key</h2>
        <p style="font-size: 14px; color: #ccc; margin-bottom: 20px;">
          We'll store it securely in your browser and only use it for fetching item market prices.
        </p>
        <input id="kw-api-input" type="text" value="${localStorage.getItem("tornAutoPriceAPIKey") || ''}" placeholder="Your API Key..." style="
          width: 100%; padding: 10px; border-radius: 4px; border: none; font-size: 16px;
          margin-bottom: 20px;" />
        <button id="kw-save-api" style="
          background: #00ccff; border: none; color: #000; padding: 10px 20px;
          font-weight: bold; border-radius: 5px; cursor: pointer;">
          Save & Continue
        </button>
      </div>
    </div>
  `;

  document.body.insertAdjacentHTML("beforeend", modalHTML);
  document.getElementById("kw-save-api").addEventListener("click", () => {
    const input = /** @type {HTMLInputElement} */ (document.getElementById("kw-api-input"));
    const value = input.value.trim();
    if (value.length >= 16) {
      localStorage.setItem("tornAutoPriceAPIKey", value);
      document.getElementById("kw-modal").remove();
      console.log("API key saved:", value);
      main();
    } else {
      input.style.border = "2px solid red";
    }
  });
}

// Function to get API key from localStorage or show modal
function getApiKey() {
  const key = localStorage.getItem("tornAutoPriceAPIKey");
  if (!key) {
    showApiKeyModal();
  } else {
    console.log("API key retrieved:", key);
    main();
  }
}

// Add settings menu item
function addMarketAutoPriceSettingsMenuItem() {
  const menu = document.querySelector('.settings-menu');
  if (!menu || document.querySelector('.market-auto-price-settings-button')) return;
  const li = document.createElement('li');
  li.className = 'link market-auto-price-settings-button';
  const a = document.createElement('a');
  a.href = '#';
  const iconDiv = document.createElement('div');
  iconDiv.className = 'icon-wrapper';
  const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svgIcon.setAttribute('class', 'default');
  svgIcon.setAttribute('fill', '#fff');
  svgIcon.setAttribute('stroke', 'transparent');
  svgIcon.setAttribute('stroke-width', '0');
  svgIcon.setAttribute('width', '16');
  svgIcon.setAttribute('height', '16');
  svgIcon.setAttribute('viewBox', '0 0 512 512');
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.setAttribute('d', 'M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z');
  svgIcon.appendChild(path);
  iconDiv.appendChild(svgIcon);
  const span = document.createElement('span');
  span.textContent = 'Item Market Auto Price Settings';
  a.appendChild(iconDiv);
  a.appendChild(span);
  li.appendChild(a);
  a.addEventListener('click', e => {
    e.preventDefault();
    document.body.click();
    showApiKeyModal();
  });
  const logoutButton = menu.querySelector('li.logout');
  if (logoutButton) {
    menu.insertBefore(li, logoutButton);
  } else {
    menu.appendChild(li);
  }
}

const menuObserver = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    if (mutation.addedNodes.length > 0) {
      for (const node of mutation.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('settings-menu')) {
          addMarketAutoPriceSettingsMenuItem();
          break;
        }
      }
    }
  });
});
menuObserver.observe(document.body, { childList: true, subtree: true });

addMarketAutoPriceSettingsMenuItem();

// Wait for the specific elements to be present
function waitForElements(selector, callback) {
  const observer = new MutationObserver(() => {
    if (document.querySelector(selector)) {
      observer.disconnect();
      callback();
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
}

// Main function to set up event listeners
function main() {
  if (mainCalled) return;
  mainCalled = true;
  console.log("Running main");
  $(document).on("click", inputSelector, (e) => {
    const input = e.target;
    if (input.getAttribute("placeholder") === "Qty") {
      addQuantity(input).catch((e) => handleError(e, input));
    } else {
      addPrice(input).catch((e) => handleError(e, input));
    }
  });
}

// Function to get the lowest price from the API
function getLowestPrice(itemId, apiKey) {
  const baseURL = "https://api.torn.com/v2/market";
  const searchParams = new URLSearchParams({
    selections: "itemmarket",
    key: apiKey,
    id: itemId,
    offset: "0",
  });
  const url = new URL(`?${searchParams.toString()}`, baseURL);
  return fetch(url)
    .then((res) => res.json())
    .then((data) => {
      if ("error" in data) throw new Error(data.error.error);
      const price = data?.itemmarket?.listings?.[0]?.price;
      if (typeof price === "number" && price >= 1) return price;
      throw new Error(`Invalid price: ${price}`);
    });
}

// Function to update the input field
function updateInput(input, value) {
  input.value = `${value}`;
  input.dispatchEvent(new Event("input", { bubbles: true }));
}

// Function to set the price
async function addPrice(input) {
  if (!(input instanceof HTMLInputElement))
    throw new Error("Input is not an HTMLInputElement");
  const apiKey = localStorage.getItem("tornAutoPriceAPIKey");
  if (!apiKey) {
    throw new Error("API key not set. Please set it in the settings.");
  }
  const row = input.closest("div[class*=itemRowWrapper]");
  const image = row?.querySelector("img");
  if (!image) throw new Error("Could not find image element");
  if (image.parentElement?.matches("[class*='glow-']"))
    throw new Warning("Skipping a glowing RW item");
  const itemId = image.src?.match(/\/images\/items\/([\d]+)\//)?.[1];
  if (!itemId) throw new Error("Could not find item ID");
  const currentLowestPrice = await getLowestPrice(itemId, apiKey);
  const priceToSet = Math.max(1, currentLowestPrice - diff);
  updateInput(input, priceToSet);
  input.classList.add("kw--price-set");
}

// Function to set the quantity to max
async function addQuantity(input) {
  if (!(input instanceof HTMLInputElement))
    throw new Error("Input is not an HTMLInputElement");
  updateInput(input, "max");
  input.classList.add("kw--price-set");
}

// Error handling
function handleError(e, input) {
  if (e instanceof Warning) {
    console.warn(e);
    input.style.outline = "2px solid yellow";
  } else {
    console.error(e);
    input.style.outline = "2px solid red";
  }
}

// Custom Warning class
class Warning extends Error {}

// Start the script by waiting for the elements
waitForElements(inputSelector, getApiKey);