Inventory Pricing

Adds market pricing for items in your inventory

当前为 2025-12-02 提交的版本,查看 最新版本

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 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();
  }
})();