Inventory Sorter

Allows you to sort your inventory in ascending/descending order.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name          Inventory Sorter
// @namespace     https://greasyfork.org/en/users/1362698-iambatman
// @description   Allows you to sort your inventory in ascending/descending order.
// @version       2.0
// @author        IAmBatman [2885239]
// @grant         GM_addStyle
// @grant         GM_setValue
// @grant         GM_getValue
// @grant         GM_deleteValue
// @match         https://www.torn.com/item.php
// ==/UserScript==

(async function () {
  "use strict";
  GM_addStyle(`
.is-container {
  height: 100%;
  margin-right: 2px;

  position: relative;

  display: flex;
  align-items: center;
}

.is-btn {
  height: 22px !important;
  padding: 0 5px !important;
  line-height: 0 !important;
}

.is-modal {
  height: 150px;
  width: 300px;
  margin-top: 8px;
/*   background-color: var(--default-bg-panel-color); */
  background-color: #333;
  border-radius: 5px;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px;

  position: absolute;


  display: grid;
  justify-items: center;
  grid-auto-rows: min-content;
}

.is-modal-header{
  font-size: 18px;
  font-weight: bold;
  line-height: 1;
  padding: 8px 0 12px 0;
  text-decoration: underline;
}

.is-modal-desc{
  font-size: 12px;
  text-align: center;
  margin: 0 12px 0 12px;
  line-height: 1;
}

.is-form{
  text-align: center;
  margin-top: 16px;

  position: relative;

  display: grid;
  align-items: center;
  justify-items: center;
  gap: 8px;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(2, 1fr);

}

.is-form-text{
  text-align: center;
  width: 160px;
  height: 28px;
  line-height: 0;
  border-radius: 5px;
  background-color: #000;
  color: #9f9f9f;
  border-color: transparent transparent #333 transparent;
  grid-column: span 2 / span 2;
}

.is-form-submit{
  border-radius: 5px !important;
  width: 48px !important;
  height: 24px !important;
  line-height: 0 !important;
  grid-column-start: 1;
  grid-row-start: 2;
}

.is-form-status{
  grid-column-start: 2;
  grid-row-start: 2;
  margin-left: -8px;
}

.is-form-delete-btn{
  cursor: pointer;
  top: 0;
  right: 0;
  transform: translateX(125%);

  position: absolute;
}

.is-form-delete-icon{
  height: 24px;
  width: 24px;
  stroke: #fff;
}
  .is-item-value{
  width: auto !important;
  padding: 0 10px 0 10px !important;
}

.is-item-value-color{
  color: var(--default-green-color);
}

.is-item-qty{
  font-weight: bold;
}

li:has(.group-arrow) .is-item-value{
  width: auto !important;
  padding: 0 30px 0 10px !important;
}
  `);

  let tabs = {};
  let itemValues = {};
  let sortState = "default";
  let posBeforeScroll = null;
  let itemValueSource;

  if (await isKeySaved()) {
    itemValueSource = "is";
    fetchApiValues();
  } else {
    itemValueSource = "tt";
  }

  let currentTab = getCurrentTabElement();
  let currentTabIndex = getCurrentTabIndex();
  recordTab();

  const itemObserver = new MutationObserver((mutationList, observer) => {
    mutationList.forEach((mutation) => {
      if (mutation.type === "childList") {
        if (mutation.addedNodes.length) {
          recordTab();
        }
      }
    });
  });

  const container = document.createElement("span");
  container.classList.add("is-container");
  container.classList.add("right");

  const btn = document.createElement("button");
  btn.classList.add("is-btn");
  btn.classList.add("torn-btn");
  btn.classList.add("dark-mode");
  btn.textContent = "IS";

  const modal = document.createElement("div");
  modal.classList.add("is-modal");
  modal.classList.add("hide");

  const modalHeader = document.createElement("p");
  modalHeader.classList.add("is-modal-header");
  modalHeader.textContent = "INVENTORY SORTER";

  const modalDescription = document.createElement("p");
  modalDescription.classList.add("is-modal-desc");
  modalDescription.textContent = `A public API key is sufficient. The key is stored on your browser, and it's not sent anywhere.`;

  modal.appendChild(modalHeader);
  modal.appendChild(modalDescription);

  const form = document.createElement("form");
  form.classList.add("is-form");

  const formTextInput = document.createElement("input");
  formTextInput.classList.add("is-form-text");

  const formSubmit = document.createElement("input");
  formSubmit.classList.add("is-form-submit");
  formSubmit.classList.add("torn-btn");
  formSubmit.classList.add("dark-mode");

  const formKeyStatusText = document.createElement("p");
  formKeyStatusText.classList.add("is-form-status");

  const formDeleteBtn = document.createElement("button");
  formDeleteBtn.classList.add("is-form-delete-btn");

  if (await isKeySaved()) {
    formKeyStatusText.textContent = "Key: Saved";
    formDeleteBtn.classList.remove("hide");
  } else {
    formKeyStatusText.textContent = "Key: Not Saved";
    formDeleteBtn.classList.add("hide");
  }

  const deleteBtnSvg = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="is-form-delete-icon">
                    <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
                </svg>`;

  formDeleteBtn.insertAdjacentHTML("afterbegin", deleteBtnSvg);

  formTextInput.setAttribute("type", "text");
  formTextInput.setAttribute("placeholder", "API Key");
  formSubmit.setAttribute("type", "button");
  formSubmit.setAttribute("value", "SAVE");
  formSubmit.disabled = true;

  form.appendChild(formTextInput);
  form.appendChild(formSubmit);
  form.appendChild(formKeyStatusText);
  form.appendChild(formDeleteBtn);
  modal.appendChild(form);

  const titleEl = document.querySelector(".title-black");
  container.appendChild(btn);
  container.appendChild(modal);
  titleEl.appendChild(container);

  itemObserver.observe(getCurrentTabContent(), {
    attributes: true,
    childList: true,
    subtree: true,
  });

  const categoryElements =
    document.querySelectorAll("#categoriesList")[0].children;

  titleEl.addEventListener("click", async (e) => {
    if (e.target !== titleEl) return;

    if (
      (await GM_getValue("invsorter", null)) !== null &&
      document.querySelector(".tt-item-price")
    ) {
      alert(
        "Please either delete your API key after clicking the 'IS' button or disable Torn Tools to be able to use Inventory Sorter!"
      );
      return;
    } else if (
      (await GM_getValue("invsorter", null)) === null &&
      !document.querySelector(".tt-item-price")
    ) {
      alert(
        "Please submit your API key after clicking the 'IS' button to be able to use Inventory Sorter!"
      );
      return;
    }

    if (!posBeforeScroll && !tabs[currentTabIndex].isFullyLoaded) {
      posBeforeScroll = window.scrollY;
      await loadTabItems();
    }

    if (tabs[currentTabIndex].isFullyLoaded) {
      sortTab();
    }
  });

  Array.from(categoryElements).forEach((el) => {
    if (el.classList.contains("no-items")) return;

    el.addEventListener("click", (e) => {
      posBeforeScroll = null;
      currentTab = getCurrentTabElement();
      currentTabIndex = getCurrentTabIndex();
      sortState = "default";

      itemObserver.disconnect();
      itemObserver.observe(getCurrentTabContent(), {
        attributes: true,
        childList: true,
        subtree: true,
      });

      recordTab();
    });
  });

  btn.addEventListener("click", async (e) => {
    modal.classList.toggle("hide");
  });

  formSubmit.addEventListener("click", async (e) => {
    e.preventDefault();
    const key = formTextInput.value;
    const url = `https://api.torn.com/key/?key=${key}&selections=info&comment=InvSorter`;

    try {
      const res = await fetch(url);

      if (!res.ok) {
        throw new Error(res.status);
      }

      const data = await res.json();

      if (data.error && data.error.error) {
        formKeyStatusText.textContent = "API Error!";
        return;
      }

      await GM_setValue("invsorter", key);

      formTextInput.value = "";
      formKeyStatusText.textContent = "Key: Saved";
      formDeleteBtn.classList.toggle("hide");
      formSubmit.disabled = true;
    } catch (err) {
      console.error(`Inventory Sorter: ${err.message}`);
    }
  });

  form.addEventListener("submit", (e) => {
    e.preventDefault();
  });

  formTextInput.addEventListener("input", async (e) => {
    if (formTextInput.value === "" && !(await isKeySaved())) {
      formSubmit.disabled = true;
    }

    if (formTextInput.value !== "" && !(await isKeySaved())) {
      formSubmit.disabled = false;
    }
  });

  formDeleteBtn.addEventListener("click", async (e) => {
    await GM_deleteValue("invsorter");

    formTextInput.value = "";
    formKeyStatusText.textContent = "Key: Not Saved";
    formDeleteBtn.classList.toggle("hide");
    formSubmit.disabled = false;
  });

  function recordTab() {
    if (tabs[currentTabIndex]?.isFullyLoaded) return;

    if (tabs[currentTabIndex]) {
      tabs[currentTabIndex].defaultOrder = [...getCurrentTabContent().children];

      if (itemValueSource === "is" && Object.keys(itemValues).length) {
        appendItemValues(tabs[currentTabIndex].defaultOrder);
      }

      return;
    }

    const newTab = {
      [currentTabIndex]: {
        isFullyLoaded: false,
        defaultOrder: [...getCurrentTabContent().children],
      },
    };

    if (itemValueSource === "is" && Object.keys(itemValues).length) {
      appendItemValues(newTab[currentTabIndex].defaultOrder);
    }

    tabs = { ...tabs, ...newTab };
  }

  function getCurrentTabElement() {
    return document.querySelector(".ui-tabs-active");
  }

  function getCurrentTabContent() {
    return document.querySelector('[aria-hidden="false"]');
  }

  function getCurrentTabIndex() {
    return Array.prototype.indexOf.call(
      currentTab.parentNode.children,
      currentTab
    );
  }

  async function fetchApiValues() {
    try {
      const apiKey = await GM_getValue("invsorter", null);
      const url = `https://api.torn.com/torn/?key=${apiKey}&selections=items&comment=InvSorter`;

      const res = await fetch(url);

      if (!res.ok) {
        throw new Error(res);
      }

      const data = await res.json();

      if (data.error && data.error.error) {
        return;
      }

      const { items } = data;

      for (const [id, item] of Object.entries(items)) {
        itemValues[id] = { name: item.name, value: item.market_value };
      }

      appendItemValues(tabs[currentTabIndex].defaultOrder);
    } catch (err) {
      console.error(`Inventory Sorter: ${err.message}`);
    }
  }

  function appendItemValues(defaultElements) {
    Array.from(defaultElements).forEach((el) => {
      if (!el.getAttribute("data-item")) return;

      if (!el.querySelector(".is-item-value")) {
        const nameWrap = el.querySelector(".name-wrap");
        const itemValue = getItemValue(el.getAttribute("data-item"));
        const valueEl = document.createElement("span");
        const totalValueEl = document.createElement("span");
        const qtyEl = el.querySelector(".item-amount");
        const bonusesEl = el.querySelector(".bonuses-wrap");

        valueEl.classList.add("is-item-value-color");
        totalValueEl.classList.add("is-item-value-color");

        let valueContainer;

        if (bonusesEl) {
          valueContainer = document.createElement("li");
          valueContainer.classList.add("is-item-value");
          bonusesEl.appendChild(valueContainer);
        } else {
          valueContainer = document.createElement("span");
          valueContainer.classList.add("is-item-value");
          valueContainer.classList.add("right");
          nameWrap.appendChild(valueContainer);
        }

        if (qtyEl.textContent === "") {
          valueEl.textContent = `${getUsdFormat(itemValue)}`;
          valueContainer.appendChild(valueEl);
        } else {
          valueEl.textContent = `${getUsdFormat(itemValue)} `;

          const itemQty = qtyEl.textContent;
          const newQtyEl = document.createElement("span");
          newQtyEl.classList.add("is-item-qty");
          newQtyEl.textContent = `x ${itemQty} = `;

          totalValueEl.textContent = `${getUsdFormat(itemQty * itemValue)}`;

          valueContainer.appendChild(valueEl);
          valueContainer.appendChild(newQtyEl);
          valueContainer.appendChild(totalValueEl);
        }
      }
    });
  }

  function getItemValue(itemId) {
    return itemValues[itemId]?.value;
  }

  function getUsdFormat(amount) {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
      maximumFractionDigits: 0,
    }).format(amount);
  }

  function sortTab() {
    const defaultOrderCopy = [...tabs[currentTabIndex].defaultOrder];

    const tabItems = [];
    for (const item of defaultOrderCopy) {
      if (item.classList.contains("ajax-item-loader")) {
        continue;
      }

      const itemId = item.getAttribute("data-item");
      let itemQty = item.getAttribute("data-qty");

      if (itemValueSource === "is") {
        if (itemQty === "") {
          itemQty = 1;
        }

        tabItems.push({ el: item, value: getItemValue(itemId) * itemQty });
      } else if (itemValueSource === "tt") {
        const value = item
          .querySelector(".tt-item-price")
          .lastChild.textContent.replace(/[^0-9.-]+/g, "");
        tabItems.push({ el: item, value });
      }
    }

    if (sortState === "default") {
      sortState = "descending";
      tabItems.sort((a, b) => b.value - a.value);
      tabItems.forEach((item) => getCurrentTabContent().appendChild(item.el));
    } else if (sortState === "descending") {
      sortState = "ascending";
      tabItems.sort((a, b) => a.value - b.value);
      tabItems.forEach((item) => getCurrentTabContent().appendChild(item.el));
    } else if (sortState === "ascending") {
      sortState = "default";
      defaultOrderCopy.forEach((item) => {
        if (!item.classList.contains("ajax-item-loader")) {
          getCurrentTabContent().appendChild(item);
        }
      });
    }
  }

  async function loadTabItems() {
    const text = document.querySelector("#load-more-items-desc").textContent;

    if (text.toLowerCase().includes("full")) {
      window.scroll(0, posBeforeScroll);
      tabs[currentTabIndex].isFullyLoaded = true;
      return;
    }

    if (text.toLowerCase().includes("load more")) {
      document.querySelector(".items-wrap").lastElementChild.scrollIntoView();
      await new Promise((resolve) => setTimeout(resolve, 500));
      return loadTabItems();
    }
  }

  async function isKeySaved() {
    return (await GM_getValue("invsorter", null)) !== null;
  }
})();