Adds market pricing for items in your inventory (handles SPA + re-renders)
目前為
// ==UserScript==
// @name Inventory Pricing
// @namespace https://greasyfork.org/users/1543605
// @version 0.2
// @description Adds market pricing for items in your inventory (handles SPA + re-renders)
// @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 LOCAL_STORAGE_TTL_MS = 5 * 60 * 1000;
const LABEL_SELECTOR = '.q-item__label';
const PRICE_CLASS = 'market-pricing';
const DATA_FLAG = 'priceInjected';
const getPluginName = () => PLUGIN_NAME.toUpperCase();
GM_addStyle(`
.${PRICE_CLASS} {
font-size: 12px;
color: #00b894;
margin: 4px 0 0 0;
opacity: 0.9;
}
`);
const fetchJSON = (url) =>
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: (response) => {
try {
const text = response.responseText;
const json = JSON.parse(text);
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 createMarketPriceElement = (price) => {
const div = document.createElement('div');
const p = document.createElement('p');
p.classList.add(PRICE_CLASS);
p.textContent = `$${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(() => {});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start, { once: true });
} else {
start();
}
})();