Inventory Pricing

Adds market pricing for items in your inventory

目前為 2025-12-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Inventory Pricing
// @namespace    https://greasyfork.org/users/1543605
// @version      0.4.8
// @description  Adds market pricing for items in your inventory
// @license      MIT
// @author       Bobb
// @match        https://www.zed.city/inventory
// @connect      api.zed.city
// @run-at       document-end
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(() => {
  'use strict';

  const PLUGIN_NAME = 'Inventory Pricing';
  const MARKET_URL = 'https://api.zed.city/getMarket';
  const LOCAL_STORAGE_NAME = 'market-prices';
  const INVENTORY_PRICING_SETTING_LOCAL_STORAGE_NAME = 'inventory-pricing-currency-settings'
  const LOCAL_STORAGE_TTL_MS = 5 * 60 * 1000; 
  const LABEL_SELECTOR = '.q-item__label';
  const PRICE_CLASS = 'market-pricing';
  const DATA_FLAG = 'priceInjected';

  const CURRENCY_OBJECT_TEMPLATE = {
    locale: "",
    style: "currency",
    currency: ""
  }

  const DEFAULT_CURRENCY_OBJECT_TEMPLATE = {
    locale: "en-US",
    style: "currency",
    currency: "USD"
  }

  const DEFAULT_CURRENCY_OBJ = new Intl.NumberFormat(DEFAULT_CURRENCY_OBJECT_TEMPLATE.locale, { style: DEFAULT_CURRENCY_OBJECT_TEMPLATE.style, currency: DEFAULT_CURRENCY_OBJECT_TEMPLATE.currency});
  let currentCurrencyObject = null;
  let currentCurrencyObjectTemplate = null;

  const COMMON_LOCALES = [
    "en-US",
    "en-GB",
    "fr-FR",
    "de-DE",
    "es-ES",
    "it-IT",
    "ja-JP",
    "zh-CN",
    "ko-KR",
    "pt-BR"
  ];

  const COMMON_CURRENCIES = [
    "USD",
    "EUR",
    "GBP",
    "JPY",
    "CNY",
    "AUD",
    "CAD",
    "CHF",
    "HKD",
    "INR"
  ];

  const getPluginName = () => PLUGIN_NAME.toUpperCase();

  GM_addStyle(`
    .${PRICE_CLASS} {
      font-size: 12px;
      color: #00b894;
      margin: 4px 0 0 0;
      opacity: 0.9;
    }
  `);

  GM_addStyle(`
    .inventory-prices-settings-modal {
      position: absolute;
      bottom: 50px;
      left: 50px;
      width: 200px;
      height: 400px;
      z-index: 2;
      background-color: rgba(30,30,30,0.8);
      padding: 20px;
    }
  `);

  GM_addStyle(`
    .inventory-prices-settings-modal__select {
      width: 100%;
    }
  `);

  const fetchJSON = (url) =>
    new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        onload: (res) => {
          try {
            const txt = res.responseText;
            const json = JSON.parse(txt);
            resolve(json);
          } catch (e) {
            console.error(`[${getPluginName()}] Error parsing JSON from ${url}:`, e);
            reject(e);
          }
        },
        onerror: (err) => {
          console.error(`[${getPluginName()}] Request failed:`, err);
          reject(err);
        },
      });
    });

  const initializeSettingsModal = () => {
    const div = document.createElement('div');
    div.classList.add('inventory-prices-settings-modal')

    const localeSelectEl = document.createElement('select')
    const currencySelectEl = document.createElement('select')

    COMMON_LOCALES.forEach(l => {
      let opt = document.createElement('option')
      opt.value = l
      opt.innerText = l

      localeSelectEl.appendChild(opt)
    })

    

    COMMON_CURRENCIES.forEach(l => {
      let opt = document.createElement('option')
      opt.value = l
      opt.innerText = l

      currencySelectEl.appendChild(opt)
    });

    

    if(!localStorage.getItem(INVENTORY_PRICING_SETTING_LOCAL_STORAGE_NAME)) {
      currentCurrencyObject = DEFAULT_CURRENCY_OBJ
      currentCurrencyObjectTemplate = DEFAULT_CURRENCY_OBJECT_TEMPLATE
      localStorage.setItem(INVENTORY_PRICING_SETTING_LOCAL_STORAGE_NAME, JSON.stringify(currentCurrencyObjectTemplate))
    } else {
      currentCurrencyObjectTemplate = JSON.parse(localStorage.getItem(INVENTORY_PRICING_SETTING_LOCAL_STORAGE_NAME))
      currentCurrencyObject = new Intl.NumberFormat(currentCurrencyObjectTemplate.locale, { style: currentCurrencyObjectTemplate.style, currency: currentCurrencyObjectTemplate.currency})
    }

    localeSelectEl.addEventListener('change', (e) => {
      currentCurrencyObjectTemplate.locale = e.target.value
      localStorage.setItem(INVENTORY_PRICING_SETTING_LOCAL_STORAGE_NAME, JSON.stringify(currentCurrencyObjectTemplate))
      currentCurrencyObject = new Intl.NumberFormat(currentCurrencyObjectTemplate.locale, { style: currentCurrencyObjectTemplate.style, currency: currentCurrencyObjectTemplate.currency})

      Array.from(document.querySelectorAll(`.${PRICE_CLASS}`)).forEach(p => {
        const price = p.getAttribute('data-market-price')
        p.textContent = currentCurrencyObject.format(price)
      })
    })

    currencySelectEl.addEventListener('change', (e) => {
      currentCurrencyObjectTemplate.currency = e.target.value
      localStorage.setItem(INVENTORY_PRICING_SETTING_LOCAL_STORAGE_NAME, JSON.stringify(currentCurrencyObjectTemplate))
      currentCurrencyObject = new Intl.NumberFormat(currentCurrencyObjectTemplate.locale, { style: currentCurrencyObjectTemplate.style, currency: currentCurrencyObjectTemplate.currency})

      Array.from(document.querySelectorAll(`.${PRICE_CLASS}`)).forEach(p => {
        const price = p.getAttribute('data-market-price')
        p.textContent = currentCurrencyObject.format(price)
      })
    })

    const localSelectLabelEl = document.createElement('label')
    localSelectLabelEl.style.display = "block"
    localSelectLabelEl.style.color = "white"
    localSelectLabelEl.innerText = "Locale"

    const currencySelectLabelEl = document.createElement('label')
    currencySelectLabelEl.style.display = "block"
    currencySelectLabelEl.style.color = "white"
    currencySelectLabelEl.innerText = "Currency"

    div.appendChild(localSelectLabelEl)
    div.appendChild(localeSelectEl)
    div.appendChild(currencySelectLabelEl)
    div.appendChild(currencySelectEl)
    document.body.appendChild(div)
  }

  const createMarketPriceElement = (price) => {
    const div = document.createElement('div');
    div.classList.add('inventory-pricing__price-wrapper')
    const p = document.createElement('p');
    p.classList.add(PRICE_CLASS);
    p.setAttribute('data-market-price', price)
    p.textContent = DEFAULT_CURRENCY_OBJ.format(price);
    div.appendChild(p);
    return div;
  };

  const now = () => Date.now();

  const getRawLocalStorage = async () => {
    const raw = localStorage.getItem(LOCAL_STORAGE_NAME);
    if (!raw) {
      await setLocalStorage(); 
      return localStorage.getItem(LOCAL_STORAGE_NAME);
    }
    return raw;
  };

  const getParsedLocalStorage = async () => {
    try {
      const raw = await getRawLocalStorage();
      if (!raw) return null;
      return JSON.parse(raw);
    } catch (e) {
      console.error(`[${getPluginName()}] Error parsing local storage:`, e);
      return null;
    }
  };

  const setLocalStorage = async () => {
    console.log(`[${getPluginName()}] Fetching market data…`);
    const res = await fetchJSON(MARKET_URL);
    
    const map = Array.isArray(res?.items)
      ? Object.fromEntries(res.items.map((i) => [String(i.name || '').trim(), i.market_price ?? null]))
      : {};
    const payload = {
      fetchedAt: now(),
      items: res?.items || [],
      map,
    };
    localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(payload));
    return payload;
  };

  const ensureMarketData = async () => {
    let data = await getParsedLocalStorage();
    const expired = !data || typeof data.fetchedAt !== 'number' || now() - data.fetchedAt > LOCAL_STORAGE_TTL_MS;
    if (expired) {
      data = await setLocalStorage();
    }
    
    if (!data.map) {
      data.map = Object.fromEntries((data.items || []).map((i) => [String(i.name || '').trim(), i.market_price ?? null]));
      localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
    }
    return data;
  };

  const getMarketPrice = async (nameRaw) => {
    try {
      const name = String(nameRaw || '').trim();
      if (!name) return null;
      const market = await ensureMarketData();
      
      if (name in market.map) return market.map[name];

      
      const lower = name.toLowerCase();
      for (const key of Object.keys(market.map)) {
        if (key.toLowerCase() === lower) return market.map[key];
      }
      return null;
    } catch (e) {
      console.error(`[${getPluginName()}] getMarketPrice error: ${e.message}`);
      return null;
    }
  };

  const injectForLabelEl = async (labelEl) => {
    try {
      
      if (labelEl.dataset[DATA_FLAG] === '1') return;

      const itemName = labelEl.textContent?.trim();
      if (!itemName) return;

      const price = await getMarketPrice(itemName);
      if (price == null) return;

      
      
      const host = labelEl.closest('.q-item__section--main') || labelEl.parentElement || labelEl;
      
      if (host.querySelector(`.${PRICE_CLASS}`)) {
        labelEl.dataset[DATA_FLAG] = '1';
        return;
      }
      host.appendChild(createMarketPriceElement(price));
      labelEl.dataset[DATA_FLAG] = '1';
    } catch (e) {
      console.error(`[${getPluginName()}] Injection error:`, e);
    }
  };

  const processInventoryOnce = () => {
    const labels = document.querySelectorAll(LABEL_SELECTOR);
    if (!labels || labels.length === 0) {
      
      return;
    }
    labels.forEach((el) => void injectForLabelEl(el));
  };

  const observeInventory = () => {
    const observer = new MutationObserver((mutations) => {
      let shouldScan = false;
      for (const m of mutations) {
        if (m.addedNodes && m.addedNodes.length > 0) {
          shouldScan = true;
          break;
        }
        if (m.type === 'attributes') {
          shouldScan = true;
          break;
        }
      }
      if (shouldScan) processInventoryOnce();
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
    });

    processInventoryOnce();
  };

  const wrapHistory = () => {
    const push = history.pushState;
    const replace = history.replaceState;
    history.pushState = function () {
      const ret = push.apply(this, arguments);
      setTimeout(processInventoryOnce, 50);
      return ret;
    };
    history.replaceState = function () {
      const ret = replace.apply(this, arguments);
      setTimeout(processInventoryOnce, 50);
      return ret;
    };
    window.addEventListener('popstate', () => setTimeout(processInventoryOnce, 50));
  };

  const start = () => {
    console.log(`[${getPluginName()}] Starting…`);
    wrapHistory();
    observeInventory();
    
    ensureMarketData().catch(() => {});

    initializeSettingsModal();
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
  } else {
    start();
  }
})();