Inventory Pricing

Adds market pricing for items in your inventory

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Inventory Pricing
// @namespace    https://greasyfork.org/users/1543605
// @version      0.5.2
// @description  Adds market pricing for items in your inventory
// @license      MIT
// @author       Bobb
// @match        https://www.zed.city/*
// @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 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')


    

    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})
    }

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

      if(currentCurrencyObjectTemplate.locale == l) {
        opt.selected = true
      } else {
        opt.selected = false
      }

      localeSelectEl.appendChild(opt)
    })

    

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

      if(currentCurrencyObjectTemplate.currency == l) {
        opt.selected = true
      } else {
        opt.selected = false
      }

      currencySelectEl.appendChild(opt)
    });


    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 = currentCurrencyObject.format(price);
    div.appendChild(p);
    return div;
  };

const repaintAllPrices = async () => {
  const data = await getParsedLocalStorage();
  if (!data || !data.map) return;

  const labels = document.querySelectorAll(LABEL_SELECTOR);
  labels.forEach((labelEl) => {
    const itemName = labelEl.textContent?.trim();
    if (!itemName) return;

    const host =
      labelEl.closest('.q-item__section--main') || labelEl.parentElement || labelEl;
    let priceEl = host.querySelector(`.${PRICE_CLASS}`);

    const price = data.map[itemName] ?? null;

    if (price == null) {
      if (priceEl) priceEl.remove();
      labelEl.dataset[DATA_FLAG] = '0';
      return;
    }

    if (!priceEl) {
      host.appendChild(createMarketPriceElement(price));
    } else {
      priceEl.setAttribute('data-market-price', price);
      priceEl.textContent = currentCurrencyObject.format(price);
    }
    labelEl.dataset[DATA_FLAG] = '1';
  });
};

const refreshMarketAndRepaint = async () => {
  try {
    await setLocalStorage();
    await repaintAllPrices();
  } catch (e) {
    console.error(`[${getPluginName()}] refreshMarketAndRepaint error:`, e);
  }
};


  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();
  };

  // Bind listeners to pagination and filter tabs to refresh market data on click
const bindUITriggers = () => {
  let debounceTimer = null;

  document.addEventListener(
    'click',
    (e) => {
      const t = e.target;

      // Pagination buttons (Quasar q-pagination)
      const hitPagination =
        t.closest('.q-pagination .q-btn') ||
        t.closest('.q-pagination [role="button"]');

      // Category/filter tabs row (the "WEAPONS / ARMOUR / ..." strip)
      // Covers .filter-btn in your screenshot and common Quasar tab roles/classes
      const hitFilters =
        t.closest('.filter-btn') ||
        t.closest('[role="tablist"] .q-tab') ||
        t.closest('[role="tab"]');

      if (hitPagination || hitFilters) {
        clearTimeout(debounceTimer);
        // Small delay lets the UI update before we repaint prices
        debounceTimer = setTimeout(refreshMarketAndRepaint, 120);
      }
    },
    true // capture phase to be more resilient with Quasar’s internal handlers
  );
};


  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();
    bindUITriggers();
    
    ensureMarketData().catch(() => {});

    initializeSettingsModal();
  };

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