Greasy Fork is available in English.
Automates pricing with support for fixed prices, API, and RRP.
// ==UserScript==
// @name Torn Junk Seller
// @namespace torn.tools
// @version 2.2.2
// @description Automates pricing with support for fixed prices, API, and RRP.
// @match https://www.torn.com/*
// @grant GM_xmlhttpRequest
// @connect greasyfork.org
// @run-at document-idle
// @license CC BY-ND 4.0
// ==/UserScript==
(function () {
'use strict';
const CURRENT_VERSION = '2.2.2';
const STORAGE_KEY_MARKET = 'tm_saved_items';
const STORAGE_KEY_BAZAAR = 'tm_saved_items_bazaar';
const CONFIG_KEY = 'tm_config';
const KEEP_QTY_KEY_MARKET = 'tm_keep_qty_market';
const KEEP_QTY_KEY_BAZAAR = 'tm_keep_qty_bazaar';
const ITEM_MARGIN_KEY = 'tm_item_margin';
const PRICE_CACHE_KEY = 'tm_price_cache';
const LOCKED_PRICES_KEY = 'tm_locked_prices';
const FIXED_PRICE_KEY = 'tm_fixed_prices';
const BULK_THRESHOLD = 200;
const BULK_DISCOUNT = 0.10;
const RRP_FALLBACK_THRESHOLD = 0.20;
function getInfoEl(row) {
const itemRow = row.querySelector('[class*="itemRow"]');
if (!itemRow) return null;
const children = [...itemRow.children];
return children.find(el => el.tagName !== 'BUTTON') ?? null;
}
function getPriceWrapper(row) {
const priceInput = row.querySelector('input[aria-label*=" price"]');
return priceInput?.closest('[class*="priceInputWrapper"]') ?? null;
}
function getNameEl(row) {
return row.querySelector('[class*="name"]') ?? row.querySelector('[class*="title"]');
}
/* ===============================
PAGE DETECTION
=============================== */
function isAddListingPage() {
const hash = window.location.hash;
return (hash.includes('/addListing') || hash.includes('/add')) && !hash.includes('/viewListing') && !hash.includes('/confirm');
}
function isConfirmPage() {
return window.location.hash.includes('/addListing/confirm');
}
function isViewListingPage() {
return window.location.hash.includes('/viewListing');
}
function isBazaarPage() {
return window.location.pathname.includes('bazaar.php') && window.location.hash.includes('/add');
}
function isManageBazaarPage() {
return window.location.pathname.includes('bazaar.php') && window.location.hash.includes('/manage');
}
function isActivePage() {
return isAddListingPage() || isBazaarPage() || isManageBazaarPage() || isViewListingPage() || isConfirmPage();
}
/* ===============================
CONFIG & STORAGE
=============================== */
let _configCache = null;
function getConfig() {
if (_configCache) return _configCache;
const defaults = { margin: 5, rounding: 50, apiKey: '', cacheTtlMin: 5, bulkDetection: true, bulkThreshold: 200 };
try {
const cfg = localStorage.getItem(CONFIG_KEY);
_configCache = cfg ? Object.assign({}, defaults, JSON.parse(cfg)) : defaults;
} catch { _configCache = defaults; }
return _configCache;
}
function saveConfig(cfg) {
_configCache = null;
_playerIdCache = null;
localStorage.setItem(CONFIG_KEY, JSON.stringify(cfg));
}
function getMargin() { return getConfig().margin ?? 5; }
function getRounding() { return getConfig().rounding ?? 50; }
function getApiKey() { return getConfig().apiKey ?? ''; }
function getCacheTtlMs() { return (getConfig().cacheTtlMin ?? 5) * 60 * 1000; }
function isBulkDetectionOn() { return getConfig().bulkDetection !== false; }
function getBulkThreshold() { return getConfig().bulkThreshold ?? BULK_THRESHOLD; }
let _fixedPriceCache = null;
function getFixedPriceMap() {
if (_fixedPriceCache) return _fixedPriceCache;
try { _fixedPriceCache = JSON.parse(localStorage.getItem(FIXED_PRICE_KEY)) || {}; }
catch { _fixedPriceCache = {}; }
return _fixedPriceCache;
}
function getFixedPrice(id) { return getFixedPriceMap()[id] ?? null; }
function setFixedPrice(id, value) {
const map = getFixedPriceMap();
if (value === null || value === '' || isNaN(value)) delete map[id];
else map[id] = Number(value);
_fixedPriceCache = map;
localStorage.setItem(FIXED_PRICE_KEY, JSON.stringify(map));
}
/* ===============================
PRICE CACHE
=============================== */
let _priceCache = null;
function loadPriceCache() {
if (_priceCache) return _priceCache;
try { _priceCache = JSON.parse(localStorage.getItem(PRICE_CACHE_KEY)) || {}; }
catch { _priceCache = {}; }
return _priceCache;
}
function savePriceCache(cache) {
_priceCache = cache;
localStorage.setItem(PRICE_CACHE_KEY, JSON.stringify(cache));
}
function getCacheEntry(itemId) {
const cache = loadPriceCache();
const entry = cache[itemId];
if (!entry) return null;
if (Date.now() - entry.ts > getCacheTtlMs()) return null;
return entry;
}
function setCacheEntry(itemId, entry) {
const cache = loadPriceCache();
cache[itemId] = { ...entry, ts: Date.now() };
const ttl = getCacheTtlMs();
const now = Date.now();
for (const id of Object.keys(cache)) {
if (now - cache[id].ts > ttl) delete cache[id];
}
savePriceCache(cache);
}
function clearCacheEntry(itemId) {
const cache = loadPriceCache();
delete cache[itemId];
savePriceCache(cache);
}
/* ===============================
LOCKED PRICES
=============================== */
let _lockedPricesCache = null;
function getLockedPrices() {
if (_lockedPricesCache) return _lockedPricesCache;
try { _lockedPricesCache = JSON.parse(localStorage.getItem(LOCKED_PRICES_KEY)) || []; }
catch { _lockedPricesCache = []; }
return _lockedPricesCache;
}
function isPriceLocked(itemId) { return getLockedPrices().includes(itemId); }
function togglePriceLock(itemId) {
const locked = getLockedPrices();
const idx = locked.indexOf(itemId);
if (idx === -1) locked.push(itemId);
else locked.splice(idx, 1);
_lockedPricesCache = locked;
localStorage.setItem(LOCKED_PRICES_KEY, JSON.stringify(locked));
return idx === -1;
}
/* ===============================
PLAYER ID
=============================== */
let _playerIdCache = null;
async function getPlayerId() {
if (_playerIdCache) return _playerIdCache;
const key = getApiKey();
if (!key) return null;
try {
const resp = await fetch(`https://api.torn.com/v2/user?key=${key}&fields=basic`);
if (!resp.ok) return null;
const data = await resp.json();
if (data.error || !data.id) return null;
_playerIdCache = data.id;
return _playerIdCache;
} catch { return null; }
}
/* ===============================
TORN API
=============================== */
async function fetchMarketData(itemId) {
const cached = getCacheEntry(itemId);
if (cached) return cached;
const key = getApiKey();
if (!key) return null;
try {
const url = `https://api.torn.com/v2/market/${itemId}/itemmarket?key=${key}&limit=10`;
const resp = await fetch(url);
if (!resp.ok) return null;
const data = await resp.json();
if (data.error) return null;
const allListings = data?.itemmarket?.listings;
if (!allListings || allListings.length === 0) return null;
const playerId = await getPlayerId();
const listings = playerId
? allListings.filter(l => l.seller?.id !== playerId)
: allListings;
if (listings.length === 0) return null;
const bulkThreshold = getBulkThreshold();
const lowest = listings[0].price;
let bulk = null;
for (const l of listings) {
if (l.amount >= bulkThreshold) {
if (!bulk || l.price < bulk.price) bulk = { price: l.price, amount: l.amount };
}
}
const entry = { lowest, bulk };
setCacheEntry(itemId, entry);
return entry;
} catch {
return null;
}
}
/* ===============================
UPDATE CHECKER (Bypass CSP)
=============================== */
function checkVersion() {
if (!window.location.href.includes('page.php?sid=ItemMarket') || !window.location.hash.includes('/addListing')) {
return;
}
if (typeof GM_xmlhttpRequest === 'undefined') {
console.error('[Junk Seller] GM_xmlhttpRequest nie jest dostepny.');
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: 'https://greasyfork.org/en/scripts/570131-torn-junk-seller.json',
onload: function (response) {
if (response.status !== 200) return;
try {
const data = JSON.parse(response.responseText);
if (data && data.version && data.version !== CURRENT_VERSION) {
createUpdateWidget(data.version);
}
} catch (e) {
console.error('[Junk Seller] Blad parsowania JSON z aktualizacja:', e);
}
},
onerror: function (e) {
console.error('[Junk Seller] Blad polaczenia z GreasyFork:', e);
}
});
}
function createUpdateWidget(latestVersion) {
if (document.getElementById('tm_update_notification')) return;
if (isConfirmPage()) return;
const btn = document.createElement('a');
btn.id = 'tm_update_notification';
btn.href = 'https://greasyfork.org/en/scripts/570131-torn-junk-seller';
btn.target = '_blank';
btn.title = `Wersja w repozytorium (${latestVersion}) rozni sie od Twojej aktualnej (${CURRENT_VERSION})! Kliknij, aby zaktualizowac.`;
btn.classList.add('tm-injected');
btn.style.cssText = `
width: 24px; position:fixed;right:0;bottom:calc(20% - 22px);z-index:9999;cursor:pointer;
background:linear-gradient(90deg, #4CAF50, #2E7D32);
padding:3px 8px;border-radius:4px 0 0 4px;display:flex;align-items:center;justify-content:center;
box-shadow: -2px 2px 6px rgba(0,0,0,0.4);text-decoration:none;
`;
btn.innerHTML = `<span style="color:white;font-family:Arial,sans-serif;font-size:10px;font-weight:bold;letter-spacing:0.5px;text-align: right;">update</span>`;
document.body.appendChild(btn);
}
/* ===============================
PRICE LOGIC
=============================== */
function parseMoney(text) {
if (!text) return 0;
return Number(text.replace('$', '').replace(/,/g, '').trim());
}
function roundToNearest(value) {
const r = getRounding();
if (!r || r <= 0) return Math.round(value);
return Math.round(value / r) * r;
}
function getItemMarginPct(itemId) {
return itemId !== undefined ? (getItemMargin(itemId) ?? getMargin()) : getMargin();
}
function computePrice(rrp, apiData, itemId) {
const fixedPrice = itemId ? getFixedPrice(itemId) : null;
if (fixedPrice !== null && fixedPrice > 0) {
return {
price: fixedPrice,
tooltip: `Fixed Price: $${fmt(fixedPrice)} (Manual Override)`,
};
}
const marginPct = getItemMarginPct(itemId);
const ts = apiData ? fmtTime(getCacheEntry(itemId)?.ts ?? Date.now()) : null;
const bulkEnabled = isBulkDetectionOn();
const bulkThreshold = getBulkThreshold();
if (!apiData) {
const price = roundToNearest(rrp * (1 + marginPct / 100));
const marginAmt = price - rrp;
return {
price,
tooltip: `RRP: $${fmt(rrp)} + $${fmt(marginAmt)} (+${marginPct}%) = $${fmt(price)}`,
};
}
const { lowest, bulk } = apiData;
let basePrice, usedRrpFallback = false;
if (bulkEnabled && bulk) {
basePrice = bulk.price * (1 - BULK_DISCOUNT);
} else {
basePrice = lowest;
}
if (!basePrice || basePrice < rrp * RRP_FALLBACK_THRESHOLD) {
basePrice = rrp;
usedRrpFallback = true;
}
const price = roundToNearest(basePrice * (1 + marginPct / 100));
const marginAmt = price - Math.round(basePrice);
const source = usedRrpFallback ? 'RRP' : 'API';
const baseLabel = usedRrpFallback ? fmt(rrp) : fmt(lowest);
let line = `${source} (${ts}): $${baseLabel} + $${fmt(marginAmt)} (+${marginPct}%) = $${fmt(price)}`;
if (bulkEnabled && bulk) {
line += `\n(Bulk seller: ${fmt(bulk.amount)}x $${fmt(bulk.price)} - -${BULK_DISCOUNT * 100}% - base $${fmt(Math.round(basePrice))})`;
} else if (!bulkEnabled && bulk) {
line += `\n(Bulk seller detected but ignored - bulk detection disabled)`;
} else if (usedRrpFallback) {
line += `\n(API $${fmt(lowest)} < RRP $${fmt(rrp)} = using RRP)`;
} else if (bulkEnabled && !bulk) {
line += `\n(No bulk seller found - threshold: ${fmt(bulkThreshold)}+ items)`;
}
return { price, tooltip: line };
}
function fmt(n) {
return Number(n).toLocaleString('en-US');
}
function fmtTime(ts) {
return new Date(ts).toTimeString().slice(0, 5);
}
/* ===============================
ITEM MARKET - RRP + apply
=============================== */
function getRRP_Market(row) {
const priceContainer = row.querySelector('[aria-label*="Recommended Retail Price"]');
if (!priceContainer) return 0;
const visiblePrice = priceContainer.querySelector('span');
if (!visiblePrice) return 0;
return parseMoney(visiblePrice.textContent);
}
function applyPriceToRow_Market(row, price, tooltip, priceOnly = false) {
const expandBtn = row.querySelector('button[aria-expanded="false"] .group-arrow');
const groupExpandBtn = expandBtn?.closest('button');
if (groupExpandBtn) {
groupExpandBtn.click();
setTimeout(() => applyPriceToRow_Market(row, price, tooltip, priceOnly), 300);
return;
}
const selectCheckbox = row.querySelector('input[id^="itemRow-selectCheckbox"]');
if (!priceOnly && selectCheckbox && !selectCheckbox.checked) selectCheckbox.click();
const inputs = row.querySelectorAll('input[data-testid="legacy-money-input"]:not([type="hidden"])');
if (!inputs.length) return;
const itemId = getItemId(row);
inputs.forEach(input => {
const hidden = input.parentElement.querySelector(
'input[data-testid="legacy-money-input"][type="hidden"]'
);
let value;
const dataMoney = input.dataset.money;
if (dataMoney === 'Infinity') {
value = price;
if (tooltip) {
const wrap = input.closest('[class*="priceInput"]') || input.parentElement;
wrap.title = tooltip;
}
} else {
if (priceOnly) return;
if (selectCheckbox) return;
const maxQty = Number(dataMoney);
const keepQty = itemId ? getKeepQty(itemId) : null;
value = (keepQty !== null && keepQty >= 0) ? Math.max(0, maxQty - keepQty) : maxQty;
}
setReactInputValue(input, value);
if (hidden) hidden.value = value;
});
}
async function setValue_Market(row, priceOnly = false) {
const itemId = getItemId(row);
const rrp = getRRP_Market(row);
const apiData = itemId ? await fetchMarketData(itemId) : null;
const { price, tooltip } = computePrice(rrp, apiData, itemId);
applyPriceToRow_Market(row, price, tooltip, priceOnly);
}
/* ===============================
VIEW LISTING - row updates
=============================== */
function refreshNameTitle(row) {
const itemId = getItemId(row);
if (!itemId) return;
const cached = getCacheEntry(itemId);
const nameEl = getNameEl(row);
if (!nameEl) return;
if (cached) {
nameEl.title = `Market lowest: $${fmt(cached.lowest)}${cached.bulk ? ` | Bulk ${fmt(cached.bulk.amount)}x @ $${fmt(cached.bulk.price)}` : ''}`;
} else {
nameEl.removeAttribute('title');
}
}
function addButton_ViewListing(row) {
const info = getInfoEl(row);
if (!info || info.dataset.tmViewAdded) return;
info.dataset.tmViewAdded = '1';
ensureStyles();
const itemId = getItemId(row);
refreshNameTitle(row);
const fixedInput = document.createElement('input');
fixedInput.type = 'text';
fixedInput.placeholder = 'Fix $';
fixedInput.title = 'Fixed price (overrides API/RRP)';
fixedInput.classList.add('tm-keep-input', 'tm-injected');
applyInputStyle(fixedInput);
fixedInput.style.marginLeft = '6px';
if (itemId) {
const savedFixed = getFixedPrice(itemId);
if (savedFixed !== null) fixedInput.value = savedFixed;
}
fixedInput.addEventListener('click', e => e.stopPropagation());
fixedInput.addEventListener('change', () => {
if (!itemId) return;
setFixedPrice(itemId, fixedInput.value.trim() || null);
setValue_Market(row, true);
});
const marginInput = document.createElement('input');
marginInput.type = 'text';
marginInput.placeholder = '%';
marginInput.title = 'Custom margin % for this item';
marginInput.classList.add('tm-keep-input', 'tm-injected');
applyInputStyle(marginInput);
marginInput.style.marginLeft = '6px';
if (itemId) {
const savedMargin = getItemMargin(itemId);
if (savedMargin !== null) marginInput.value = savedMargin;
}
marginInput.addEventListener('click', e => e.stopPropagation());
marginInput.addEventListener('change', () => {
if (!itemId) return;
setItemMargin(itemId, marginInput.value.trim() || null);
clearCacheEntry(itemId);
setValue_Market(row, true).then(() => refreshNameTitle(row));
});
const netLabel = document.createElement('span');
netLabel.classList.add('tm-row-net', 'tm-injected');
netLabel.style.cssText = 'margin-left:6px;font-size:10px;color:#8BC34A;line-height:1.2;text-align:right;vertical-align:middle;white-space:nowrap;width:55px;display:flex;align-items:center;justify-content:flex-end;';
function refreshNetLabel() {
const priceHidden = row.querySelector('input[data-money="Infinity"][type="hidden"]');
const qtyHidden = row.querySelector('input[type="hidden"]:not([data-money="Infinity"])');
if (!priceHidden || !qtyHidden) { netLabel.textContent = ''; return; }
const price = Number(priceHidden.value);
const qty = Number(qtyHidden.value);
if (!price || !qty) { netLabel.textContent = ''; return; }
const isAnon = !!row.querySelector('[class*="checkboxWrapper"][class*="active"]');
const feeRate = isAnon ? 0.15 : 0.05;
const net = Math.round(price * qty * (1 - feeRate));
netLabel.innerHTML = `$${fmt(net)}`;
netLabel.title = fmt(Math.round(price * qty))+"$ - "+(fmt(Math.round((price * qty)*(feeRate))))+"$";
}
refreshNetLabel();
const btnA = document.createElement('button');
btnA.textContent = 'A';
btnA.title = 'Refresh price (API)';
btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;margin-left:3px;';
btnA.classList.add('tm-injected');
btnA.addEventListener('click', e => {
e.stopPropagation();
if (itemId) clearCacheEntry(itemId);
setValue_Market(row, true).then(() => { refreshNameTitle(row); refreshNetLabel(); updateViewListingSummary(); });
});
const btnL = document.createElement('button');
btnL.classList.add('tm-injected');
btnL.style.cssText = 'padding:0 2px;cursor:pointer;margin-left:3px;background:none;border:none;line-height:1;vertical-align:middle;';
const svgLocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
const svgUnlocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFA726" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>`;
function updateLockBtn() {
const locked = itemId && isPriceLocked(itemId);
btnL.innerHTML = locked ? svgLocked : svgUnlocked;
btnL.title = locked ? 'Unlock price' : 'Lock price';
marginInput.disabled = !!locked;
fixedInput.disabled = !!locked;
marginInput.style.opacity = locked ? '0.4' : '1';
fixedInput.style.opacity = locked ? '0.4' : '1';
}
updateLockBtn();
btnL.addEventListener('click', e => {
e.stopPropagation();
if (!itemId) return;
togglePriceLock(itemId);
updateLockBtn();
});
info.appendChild(fixedInput);
info.appendChild(marginInput);
info.appendChild(netLabel);
info.appendChild(btnA);
info.appendChild(btnL);
}
/* ===============================
BAZAAR - ADD
=============================== */
function getRRP_Bazaar(row) {
const infoWrap = row.querySelector('.info-wrap[title="Market value"]');
if (!infoWrap) return 0;
const text = infoWrap.textContent.split('|')[0].trim();
return parseMoney(text);
}
function setValue_Bazaar(row) {
const itemId = getItemId_Bazaar(row);
const rrp = getRRP_Bazaar(row);
const priceInput = row.querySelector('.price input.input-money:not([type="hidden"])');
const priceHidden = row.querySelector('.price input.input-money[type="hidden"]');
if (priceInput) {
const { price } = computePrice(rrp, null, itemId);
setReactInputValue(priceInput, price);
if (priceHidden) priceHidden.value = price;
}
const checkbox = row.querySelector('input[type="checkbox"][name="amount"]');
const amountInput = row.querySelector('input[type="text"][name="amount"]');
if (checkbox) {
if (!checkbox.checked) checkbox.click();
} else if (amountInput) {
const maxQty = Number(row.querySelector('.item-amount.qty')?.textContent?.trim() ?? 0);
const keepQty = itemId ? getKeepQty(itemId) : null;
const qty = (keepQty !== null && keepQty >= 0) ? Math.max(0, maxQty - keepQty) : maxQty;
setReactInputValue(amountInput, qty);
}
}
/* ===============================
BAZAAR - MANAGE
=============================== */
function getRRP_ManageBazaar(row) {
const rrpEl = row.querySelector('[class*="rrp"]');
if (!rrpEl) return 0;
return parseMoney(rrpEl.textContent);
}
function getItemId_ManageBazaar(row) {
const img = row.querySelector('img[src*="/images/items/"]');
if (!img) return null;
const match = (img.src || img.getAttribute('srcset') || '').match(/items\/(\d+)\//);
return match ? match[1] : null;
}
async function setValue_ManageBazaar(row, priceOnly = false) {
const itemId = getItemId_ManageBazaar(row);
const rrp = getRRP_ManageBazaar(row);
const apiData = itemId ? await fetchMarketData(itemId) : null;
const { price, tooltip } = computePrice(rrp, apiData, itemId);
applyPriceToRow_ManageBazaar(row, price, tooltip);
}
function applyPriceToRow_ManageBazaar(row, price, tooltip) {
const priceInput = row.querySelector('[class*="price"] input[data-testid="legacy-money-input"]:not([type="hidden"])');
const priceHidden = row.querySelector('[class*="price"] input[data-testid="legacy-money-input"][type="hidden"]');
if (!priceInput) return;
if (tooltip) {
const wrap = priceInput.closest('[class*="price"]') || priceInput.parentElement;
wrap.title = tooltip;
}
setReactInputValue(priceInput, price);
if (priceHidden) priceHidden.value = price;
}
function addControls_ManageBazaar(row) {
const itemEl = row.querySelector('[class*="item"]');
if (!itemEl || itemEl.dataset.tmManageAdded) return;
itemEl.dataset.tmManageAdded = '1';
ensureStyles();
const itemId = getItemId_ManageBazaar(row);
const wrap = document.createElement('div');
wrap.classList.add('tm-injected');
wrap.style.cssText = 'display:flex;align-items:center;gap:3px;margin-left:4px;';
const fixedInput = document.createElement('input');
fixedInput.type = 'text';
fixedInput.placeholder = 'Fix $';
fixedInput.title = 'Fixed price (overrides API/RRP)';
fixedInput.classList.add('tm-keep-input', 'tm-injected');
applyInputStyle(fixedInput);
if (itemId) {
const saved = getFixedPrice(itemId);
if (saved !== null) fixedInput.value = saved;
}
fixedInput.addEventListener('click', e => e.stopPropagation());
fixedInput.addEventListener('change', () => {
if (!itemId) return;
setFixedPrice(itemId, fixedInput.value.trim() || null);
setValue_ManageBazaar(row);
});
const marginInput = document.createElement('input');
marginInput.type = 'text';
marginInput.placeholder = '%';
marginInput.title = 'Custom margin % for this item';
marginInput.classList.add('tm-keep-input', 'tm-injected');
applyInputStyle(marginInput);
if (itemId) {
const saved = getItemMargin(itemId);
if (saved !== null) marginInput.value = saved;
}
marginInput.addEventListener('click', e => e.stopPropagation());
marginInput.addEventListener('change', () => {
if (!itemId) return;
setItemMargin(itemId, marginInput.value.trim() || null);
clearCacheEntry(itemId);
setValue_ManageBazaar(row).then(() => refreshNameTitle_ManageBazaar(row));
});
const btnA = document.createElement('button');
btnA.textContent = 'A';
btnA.title = 'Auto-price (API/RRP)';
btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;';
btnA.classList.add('tm-injected');
btnA.addEventListener('click', e => {
e.stopPropagation();
if (itemId) clearCacheEntry(itemId);
setValue_ManageBazaar(row).then(() => refreshNameTitle_ManageBazaar(row));
});
const btnL = document.createElement('button');
btnL.classList.add('tm-injected');
btnL.style.cssText = 'padding:0 2px;cursor:pointer;background:none;border:none;line-height:1;vertical-align:middle;';
const svgLocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
const svgUnlocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFA726" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>`;
function updateLockBtn() {
const locked = itemId && isPriceLocked(itemId);
btnL.innerHTML = locked ? svgLocked : svgUnlocked;
btnL.title = locked ? 'Unlock price' : 'Lock price';
marginInput.disabled = !!locked;
fixedInput.disabled = !!locked;
marginInput.style.opacity = locked ? '0.4' : '1';
fixedInput.style.opacity = locked ? '0.4' : '1';
}
updateLockBtn();
btnL.addEventListener('click', e => {
e.stopPropagation();
if (!itemId) return;
togglePriceLock(itemId);
updateLockBtn();
});
wrap.appendChild(fixedInput);
wrap.appendChild(marginInput);
wrap.appendChild(btnA);
wrap.appendChild(btnL);
// Insert wrap into the price cell so controls sit next to the input
const priceCell = row.querySelector('[class*="price"]');
if (priceCell) {
priceCell.style.display = 'flex';
priceCell.style.alignItems = 'center';
priceCell.appendChild(wrap);
} else {
itemEl.appendChild(wrap);
}
// Auto-apply if item is saved
if (itemId && isItemSaved(itemId) && !isPriceLocked(itemId) && !row.dataset.tmAutoApplied) {
row.dataset.tmAutoApplied = '1';
setValue_ManageBazaar(row);
}
}
function refreshNameTitle_ManageBazaar(row) {
const itemId = getItemId_ManageBazaar(row);
if (!itemId) return;
const cached = getCacheEntry(itemId);
const descEl = row.querySelector('[class*="desc"]');
if (!descEl) return;
if (cached) {
descEl.title = `Market lowest: $${fmt(cached.lowest)}${cached.bulk ? ` | Bulk ${fmt(cached.bulk.amount)}x @ $${fmt(cached.bulk.price)}` : ''}`;
} else {
descEl.removeAttribute('title');
}
}
function scanRows_ManageBazaar() {
document.querySelectorAll('[data-testid="sortable-item"]').forEach(row => {
addControls_ManageBazaar(row);
});
}
function createRefreshAllWidget_ManageBazaar() {
if (document.getElementById('tm_refresh_all_btn')) return;
const btn = document.createElement('div');
btn.id = 'tm_refresh_all_btn';
btn.title = 'Refresh all bazaar prices (API)';
btn.classList.add('tm-injected');
btn.style.cssText = `
position:fixed;right:0;bottom:calc(20% + 45px);z-index:9999;cursor:pointer;
background:linear-gradient(90deg, rgb(2,36,74), rgb(3,111,201));
padding:8px;border-radius:6px;
`;
btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>`;
btn.addEventListener('click', async () => {
btn.style.opacity = '0.5';
btn.style.pointerEvents = 'none';
const rows = [...document.querySelectorAll('[data-testid="sortable-item"]')];
for (const row of rows) {
const itemId = getItemId_ManageBazaar(row);
if (itemId && isPriceLocked(itemId)) continue;
if (itemId) clearCacheEntry(itemId);
await setValue_ManageBazaar(row);
refreshNameTitle_ManageBazaar(row);
}
btn.style.opacity = '1';
btn.style.pointerEvents = '';
});
document.body.appendChild(btn);
}
/* ===============================
setReactInputValue
=============================== */
function setReactInputValue(input, value) {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
nativeSetter.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
/* ===============================
LOCAL STORAGE HELPERS
=============================== */
function getStorageKey() { return (isBazaarPage() || isManageBazaarPage()) ? STORAGE_KEY_BAZAAR : STORAGE_KEY_MARKET; }
let _savedItemsCache = {};
function getSavedItems(key) {
const k = key ?? getStorageKey();
if (_savedItemsCache[k]) return _savedItemsCache[k];
try { _savedItemsCache[k] = JSON.parse(localStorage.getItem(k)) || []; }
catch { _savedItemsCache[k] = []; }
return _savedItemsCache[k];
}
function saveItems(items, key) {
const k = key ?? getStorageKey();
_savedItemsCache[k] = items;
localStorage.setItem(k, JSON.stringify(items));
}
function addItem(id) {
const items = getSavedItems();
if (!items.includes(id)) { items.push(id); saveItems(items); }
}
function removeItem(id) { saveItems(getSavedItems().filter(i => i !== id)); }
function isItemSaved(id) { return getSavedItems().includes(id); }
function getKeepQtyKey() { return (isBazaarPage() || isManageBazaarPage()) ? KEEP_QTY_KEY_BAZAAR : KEEP_QTY_KEY_MARKET; }
let _keepQtyCache = {};
function getKeepQtyMap() {
const k = getKeepQtyKey();
if (_keepQtyCache[k]) return _keepQtyCache[k];
try { _keepQtyCache[k] = JSON.parse(localStorage.getItem(k)) || {}; }
catch { _keepQtyCache[k] = {}; }
return _keepQtyCache[k];
}
function getKeepQty(id) { return getKeepQtyMap()[id] ?? null; }
function setKeepQty(id, value) {
const map = getKeepQtyMap();
if (value === null || value === '' || isNaN(value)) delete map[id];
else map[id] = Number(value);
_keepQtyCache[getKeepQtyKey()] = map;
localStorage.setItem(getKeepQtyKey(), JSON.stringify(map));
}
function removeKeepQty(id) { setKeepQty(id, null); }
let _itemMarginCache = null;
function getItemMarginMap() {
if (_itemMarginCache) return _itemMarginCache;
try { _itemMarginCache = JSON.parse(localStorage.getItem(ITEM_MARGIN_KEY)) || {}; }
catch { _itemMarginCache = {}; }
return _itemMarginCache;
}
function getItemMargin(id) { return getItemMarginMap()[id] ?? null; }
function setItemMargin(id, value) {
const map = getItemMarginMap();
if (value === null || value === '' || isNaN(value)) delete map[id];
else map[id] = Number(value);
_itemMarginCache = map;
localStorage.setItem(ITEM_MARGIN_KEY, JSON.stringify(map));
}
function removeItemMargin(id) { setItemMargin(id, null); }
function bustAllCaches() {
_configCache = null; _fixedPriceCache = null; _priceCache = null;
_lockedPricesCache = null; _savedItemsCache = {}; _keepQtyCache = {};
_itemMarginCache = null; _playerIdCache = null;
}
/* ===============================
IMPORT / EXPORT
=============================== */
function exportAll() {
return JSON.stringify({
market: getSavedItems(STORAGE_KEY_MARKET),
bazaar: getSavedItems(STORAGE_KEY_BAZAAR),
keepMarket: JSON.parse(localStorage.getItem(KEEP_QTY_KEY_MARKET) || '{}'),
keepBazaar: JSON.parse(localStorage.getItem(KEEP_QTY_KEY_BAZAAR) || '{}'),
itemMargin: JSON.parse(localStorage.getItem(ITEM_MARGIN_KEY) || '{}'),
fixedPrices: JSON.parse(localStorage.getItem(FIXED_PRICE_KEY) || '{}'),
}, null, 2);
}
function importAll(json) {
try {
const d = JSON.parse(json);
if (d.market) saveItems(d.market, STORAGE_KEY_MARKET);
if (d.bazaar) saveItems(d.bazaar, STORAGE_KEY_BAZAAR);
if (d.keepMarket) localStorage.setItem(KEEP_QTY_KEY_MARKET, JSON.stringify(d.keepMarket));
if (d.keepBazaar) localStorage.setItem(KEEP_QTY_KEY_BAZAAR, JSON.stringify(d.keepBazaar));
if (d.itemMargin) localStorage.setItem(ITEM_MARGIN_KEY, JSON.stringify(d.itemMargin));
if (d.fixedPrices) localStorage.setItem(FIXED_PRICE_KEY, JSON.stringify(d.fixedPrices));
alert('Import OK');
bustAllCaches();
} catch {
alert('Invalid JSON');
}
}
/* ===============================
ITEM HELPERS
=============================== */
function getItemId(row) {
const img = row.querySelector('img.torn-item');
if (!img) return null;
const match = img.src.match(/items\/(\d+)\//);
return match ? match[1] : null;
}
function getItemId_Bazaar(row) {
const img = row.querySelector("img[src*='/images/items/']");
if (!img) return null;
const match = img.src.match(/items\/(\d+)\//);
return match ? match[1] : null;
}
/* ===============================
STYLES
=============================== */
function applyInputStyle(el) {
Object.assign(el.style, {
width: '42px', padding: '5px 3px',
background: 'linear-gradient(0deg, rgb(17,17,17) 0%, rgb(0,0,0) 100%)',
color: 'rgb(255,255,255)', textAlign: 'center',
border: '0.8px solid rgb(68,68,68)', borderRadius: '5px',
fontSize: '12px', fontFamily: 'Arial, Helvetica, sans-serif',
lineHeight: '14px', verticalAlign: 'middle',
appearance: 'none', MozAppearance: 'textfield', boxSizing: 'border-box',
});
}
function ensureStyles() {
if (document.getElementById('tm-keep-input-style')) return;
const s = document.createElement('style');
s.id = 'tm-keep-input-style';
s.textContent = [
'input.tm-keep-input::-webkit-outer-spin-button,',
'input.tm-keep-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }',
'input.tm-keep-input::placeholder { text-align: center; font-size: 10px; }',
'.d .items-wrap .category-wrap ul.items-cont > li { position: relative; }',
/* Manage Bazaar - widen price column in header and rows */
'[data-testid="cells-header"] [class*="price"] { width: 256px !important; }',
'[data-testid="sortable-item"] [class*="price"] { width: 260px !important; }',
/* Manage Bazaar - fix price input padding */
'.d .input-money-group input.input-money:not([type="file"]):not([type="submit"]):not([type="button"]) { padding: 5px 6px !important; }',
].join('\n');
document.head.appendChild(s);
}
/* ===============================
CONTROLS BUILDER
=============================== */
function buildControls(itemId, onAuto, onSave, onDelete, bazaar) {
ensureStyles();
const col = document.createElement('div');
col.classList.add('tm-injected');
col.style.cssText = 'display:flex;align-items:center;gap:4px;margin-left:6px;';
if (bazaar) {
Object.assign(col.style, {
position: 'absolute', left: 'calc(100% - 6px)',
background: '#333', height: '33px',
border: '1px solid black', borderBottom: 'none',
top: '-1px', paddingLeft: '3px',
});
}
const fixedInput = document.createElement('input');
fixedInput.type = 'text';
fixedInput.placeholder = 'Fix $';
fixedInput.title = 'Fixed price (overrides API/RRP)';
fixedInput.classList.add('tm-keep-input', 'tm-injected');
applyInputStyle(fixedInput);
fixedInput.style.display = 'none';
const keepInput = document.createElement('input');
keepInput.type = 'text';
keepInput.placeholder = 'Keep';
keepInput.title = 'Amount to keep after sale';
keepInput.classList.add('tm-keep-input', 'tm-injected');
applyInputStyle(keepInput);
keepInput.style.display = 'none';
const marginInput = document.createElement('input');
marginInput.type = 'text';
marginInput.placeholder = '%';
marginInput.title = 'Custom margin % for this item';
marginInput.classList.add('tm-keep-input', 'tm-injected');
applyInputStyle(marginInput);
marginInput.style.display = 'none';
const btnA = document.createElement('button');
btnA.textContent = 'A';
btnA.title = 'Auto-price';
btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;';
btnA.classList.add('tm-injected');
btnA.addEventListener('click', e => { e.stopPropagation(); onAuto(); });
const btnToggle = document.createElement('button');
btnToggle.style.cssText = 'padding:0 4px;cursor:pointer;margin-right:5px;';
btnToggle.classList.add('tm-injected');
if (itemId) {
const savedKeep = getKeepQty(itemId);
if (savedKeep !== null) keepInput.value = savedKeep;
const savedMargin = getItemMargin(itemId);
if (savedMargin !== null) marginInput.value = savedMargin;
const savedFixed = getFixedPrice(itemId);
if (savedFixed !== null) fixedInput.value = savedFixed;
}
fixedInput.addEventListener('click', e => e.stopPropagation());
fixedInput.addEventListener('change', () => {
if (!itemId) return;
setFixedPrice(itemId, fixedInput.value.trim() || null);
onAuto();
});
keepInput.addEventListener('click', e => e.stopPropagation());
keepInput.addEventListener('change', () => {
if (!itemId) return;
setKeepQty(itemId, keepInput.value.trim() || null);
onAuto();
});
marginInput.addEventListener('click', e => e.stopPropagation());
marginInput.addEventListener('change', () => {
if (!itemId) return;
setItemMargin(itemId, marginInput.value.trim() || null);
onAuto();
});
function updateToggle() {
if (!itemId) return;
if (isItemSaved(itemId)) {
btnToggle.textContent = 'D'; btnToggle.title = 'Remove';
btnToggle.style.color = '#F44336';
fixedInput.style.display = '';
keepInput.style.display = '';
marginInput.style.display = '';
} else {
btnToggle.textContent = 'S'; btnToggle.title = 'Save';
btnToggle.style.color = '#4CAF50';
fixedInput.style.display = 'none';
keepInput.style.display = 'none';
marginInput.style.display = 'none';
}
}
btnToggle.addEventListener('click', e => {
e.stopPropagation();
if (!itemId) return;
if (isItemSaved(itemId)) {
removeItem(itemId); removeKeepQty(itemId); removeItemMargin(itemId);
setFixedPrice(itemId, null);
onDelete();
} else {
onSave(); addItem(itemId);
}
updateToggle();
});
updateToggle();
col.appendChild(fixedInput);
col.appendChild(keepInput);
col.appendChild(marginInput);
col.appendChild(btnA);
col.appendChild(btnToggle);
return col;
}
/* ===============================
CONFIRM PAGE - net price summary
=============================== */
function getConfirmFeeRate() {
const activeAnon = document.querySelector('[class*="anonymousPreview"][class*="active"]');
const summaryText = document.querySelector('[class*="text"]')?.textContent ?? '';
const hasAnonFee = activeAnon !== null || summaryText.includes('anonymity fee');
return hasAnonFee ? 0.15 : 0.05;
}
function parseQty(titleEl) {
const titleWrapper = titleEl?.closest('[class*="title"]');
if (!titleWrapper) return 1;
const spans = titleWrapper.querySelectorAll('span');
for (const s of spans) {
const m = s.textContent.trim().match(/^x(\d+)$/);
if (m) return parseInt(m[1], 10);
}
return 1;
}
function injectConfirmNetPrices() {
if (document.querySelector('.tm-confirm-injected')) return;
const feeRate = getConfirmFeeRate();
const rows = document.querySelectorAll('[class*="itemRowWrapper"]');
let grandTotal = 0;
rows.forEach(row => {
if (row.querySelector('.tm-confirm-injected')) return;
const priceEl = row.querySelector('[class*="price___"]');
const nameEl = row.querySelector('[class*="name"]');
const previewEl = row.querySelector('[class*="pricePreview"]');
if (!priceEl) return;
if (previewEl) {
previewEl.style.flexDirection = 'column';
previewEl.style.placeSelf = 'flex-start';
previewEl.style.alignItems = 'flex-end';
}
const unitPrice = parseMoney(priceEl.textContent);
const qty = parseQty(nameEl);
const gross = unitPrice * qty;
const net = Math.round(gross * (1 - feeRate));
grandTotal += net;
const netEl = document.createElement('div');
netEl.className = 'tm-confirm-injected tm-injected';
netEl.textContent = `$${fmt(net)}`;
netEl.title = `${qty} x $${fmt(unitPrice)} - ${feeRate * 100}% fee = $${fmt(net)}`;
netEl.style.cssText = `
font-size: 11px;
color: #8BC34A;
margin-top: 1px;
text-align: right;
`;
priceEl.parentElement.appendChild(netEl);
});
injectGrandTotal(grandTotal, feeRate);
}
function injectGrandTotal(grandTotal, feeRate) {
const textBlock = document.querySelector('[class*="text___"]');
if (!textBlock || textBlock.querySelector('.tm-grand-total')) return;
const totalEl = document.createElement('p');
totalEl.className = 'tm-grand-total tm-injected';
totalEl.style.cssText = `
margin-top: 6px;
font-size: 12px;
color: #8BC34A;
border-top: 1px solid #333;
padding-top: 6px;
`;
totalEl.innerHTML = `Net to receive: <b style="color:#8BC34A;">$${fmt(grandTotal)}</b> <span style="color:#777;font-size:11px;">(after ${feeRate * 100}% fee)</span>`;
textBlock.appendChild(totalEl);
}
function scanRows_Confirm() {
injectConfirmNetPrices();
}
/* ===============================
INIT & SCAN
=============================== */
function addButton_Market(row) {
const info = getInfoEl(row);
if (!info || info.dataset.tmButtonAdded) return;
info.dataset.tmButtonAdded = '1';
const priceWrapper = getPriceWrapper(row);
if (priceWrapper) {
priceWrapper.dataset.tmOrigWidth = priceWrapper.style.width || '';
priceWrapper.style.width = '';
}
const itemId = getItemId(row);
const col = buildControls(
itemId,
() => setValue_Market(row),
() => setValue_Market(row),
() => {},
false
);
info.appendChild(col);
}
function addButton_Bazaar(row) {
const actionsWrap = row.querySelector('.actions-main-wrap');
if (!actionsWrap || actionsWrap.dataset.tmButtonAdded) return;
actionsWrap.dataset.tmButtonAdded = '1';
const itemId = getItemId_Bazaar(row);
const col = buildControls(
itemId,
() => setValue_Bazaar(row),
() => setValue_Bazaar(row),
() => {},
true
);
actionsWrap.appendChild(col);
}
function autoApplyForSaved_Market(row) {
const itemId = getItemId(row);
if (!itemId || !isItemSaved(itemId) || row.dataset.tmAutoApplied) return;
row.dataset.tmAutoApplied = '1';
setValue_Market(row);
}
function autoApplyForSaved_Bazaar(row) {
const itemId = getItemId_Bazaar(row);
if (!itemId || !isItemSaved(itemId) || row.dataset.tmAutoApplied) return;
row.dataset.tmAutoApplied = '1';
setValue_Bazaar(row);
}
function scanRows() {
if (isManageBazaarPage()) scanRows_ManageBazaar();
else if (isBazaarPage()) scanRows_Bazaar();
else if (isViewListingPage()) scanRows_ViewListing();
else if (isConfirmPage()) scanRows_Confirm();
else scanRows_Market();
}
function cleanup() {
document.querySelectorAll('.tm-injected').forEach(el => el.remove());
document.querySelectorAll('[data-tm-button-added]').forEach(el => { delete el.dataset.tmButtonAdded; });
document.querySelectorAll('[data-tm-view-added]').forEach(el => { delete el.dataset.tmViewAdded; });
document.querySelectorAll('[data-tm-manage-added]').forEach(el => { delete el.dataset.tmManageAdded; });
document.querySelectorAll('[class*="priceInputWrapper"][data-tm-orig-width]').forEach(wrapper => {
wrapper.style.width = wrapper.dataset.tmOrigWidth;
delete wrapper.dataset.tmOrigWidth;
});
document.querySelectorAll('[data-tm-auto-applied]').forEach(el => { delete el.dataset.tmAutoApplied; });
document.getElementById('tm-keep-input-style')?.remove();
document.getElementById('tm_settings')?.remove();
document.getElementById('tm_settings_panel')?.remove();
document.getElementById('tm_refresh_all_btn')?.remove();
document.getElementById('tm_update_notification')?.remove();
}
/* ===============================
REFRESH ALL WIDGET (viewListing)
=============================== */
function createRefreshAllWidget() {
if (document.getElementById('tm_refresh_all_btn')) return;
const btn = document.createElement('div');
btn.id = 'tm_refresh_all_btn';
btn.title = 'Refresh all listing prices (API)';
btn.style.cssText = `
position:fixed;right:0;bottom:calc(20% + 45px);z-index:9999;cursor:pointer;
background:linear-gradient(90deg, rgb(2,36,74), rgb(3,111,201));
padding:8px;border-radius:6px;
`;
btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>`;
btn.addEventListener('click', async () => {
btn.style.opacity = '0.5';
btn.style.pointerEvents = 'none';
const rows = getViewListingRows();
rows.forEach(row => {
const itemId = getItemId(row);
if (itemId && !isPriceLocked(itemId)) clearCacheEntry(itemId);
});
for (const row of rows) {
const itemId = getItemId(row);
if (itemId && isPriceLocked(itemId)) continue;
await setValue_Market(row, true);
refreshNameTitle(row);
}
btn.style.opacity = '1';
btn.style.pointerEvents = '';
updateViewListingSummary();
});
document.body.appendChild(btn);
}
function getViewListingRows() {
const rows = [];
document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
.forEach(button => {
const row = button.closest('div');
if (!row) return;
const wrapper = row.parentElement;
if (wrapper) rows.push(wrapper);
});
return rows;
}
function createWidget() {
const btn = document.createElement('div');
btn.id = 'tm_settings';
btn.title = 'Junk Seller Settings';
btn.style.cssText = `
position:fixed;right:0;bottom:20%;z-index:9999;cursor:pointer;
background:linear-gradient(90deg,#4a0202,#c90319);
padding:8px;border-radius:6px;
`;
btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g fill="white"><g transform="scale(1.5)" style="transform-origin:12px center;">
<path d="M1443,19.674a8.407,8.407,0,0,0-.659-1.593,2.576,2.576,0,0,1-2.582-.84,2.492,2.492,0,0,1-.65-2.582,8.4,8.4,0,0,0-1.592-.659,2.828,2.828,0,0,1-5.034,0,8.4,8.4,0,0,0-1.592.659,2.492,2.492,0,0,1-.65,2.582,2.578,2.578,0,0,1-2.582.841,8.371,8.371,0,0,0-.659,1.592A2.722,2.722,0,0,1,1428.539,22,3.015,3.015,0,0,1,1427,24.517a8.4,8.4,0,0,0,.659,1.592,2.595,2.595,0,0,1,3.232,3.232,8.338,8.338,0,0,0,1.592.659,2.828,2.828,0,0,1,5.034,0,8.4,8.4,0,0,0,1.592-.659,2.492,2.492,0,0,1-.65-2.582,2.576,2.576,0,0,1,2.582-.84,8.407,8.407,0,0,0,.659-1.593A2.722,2.722,0,0,1,1441.461,22,2.722,2.722,0,0,1,1443,19.674Zm-8,5.805A3.479,3.479,0,1,1,1438.479,22,3.48,3.48,0,0,1,1435,25.479Z" transform="translate(-1423 -10)"/>
</g></g></svg>`;
const panel = document.createElement('div');
panel.id = 'tm_settings_panel';
const cfg = getConfig();
panel.style.cssText = `
position:fixed;right:50px;bottom:20%;background:#222;color:white;
padding:0;display:none;z-index:9999;font-size:12px;border-radius:6px;
min-width:240px;font-family:Arial,sans-serif;border:1px solid #444;
`;
panel.innerHTML = `
<div style="display:flex;border-bottom:1px solid #444;">
<button class="tm-tab active" data-tab="pricing" style="${tabBtnStyle(true)}">Pricing</button>
<button class="tm-tab" data-tab="io" style="${tabBtnStyle(false)}">Import/Export</button>
<button class="tm-tab" data-tab="api" style="${tabBtnStyle(false)}">API</button>
</div>
<div class="tm-tab-content" data-content="pricing" style="padding:10px;display:block;">
<div style="margin-bottom:8px;">
<label>Margin %</label><br>
<input id="tm_margin" type="text" value="${cfg.margin}"
style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
</div>
<div style="margin-bottom:8px;">
<label>Rounding</label><br>
<input id="tm_rounding" type="text" value="${cfg.rounding}"
style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
</div>
<div style="margin-bottom:8px;">
<label>API cache (minutes)</label><br>
<input id="tm_cache_ttl" type="text" value="${cfg.cacheTtlMin}"
style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
</div>
<div style="margin-bottom:8px;display:flex;align-items:center;gap:6px;">
<input id="tm_bulk_detection" type="checkbox" ${cfg.bulkDetection !== false ? 'checked' : ''}
style="cursor:pointer;width:14px;height:14px;accent-color:#2196F3;">
<label for="tm_bulk_detection" style="cursor:pointer;">
Bulk seller detection
<span style="display:block;font-size:10px;color:#aaa;margin-top:1px;">
Apply -10% discount when a seller has many items
</span>
</label>
</div>
<div style="margin-bottom:12px;">
<label>Bulk seller threshold (items)</label><br>
<input id="tm_bulk_threshold" type="text" value="${cfg.bulkThreshold ?? 200}"
style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
<span style="display:block;font-size:10px;color:#aaa;margin-top:3px;">
Min. quantity listed to count as bulk seller
</span>
</div>
<button id="tm_save_cfg" style="${saveBtnStyle()}">Save</button>
</div>
<div class="tm-tab-content" data-content="io" style="padding:10px;display:none;">
<div style="margin-bottom:6px;color:#aaa;font-size:11px;">
Exports and imports item lists and per-item Keep & Margin settings.
</div>
<textarea id="tm_io_textarea"
style="width:100%;height:80px;resize:vertical;box-sizing:border-box;
background:#111;color:white;border:1px solid #555;border-radius:3px;
font-size:11px;padding:4px;"></textarea>
<div style="display:flex;gap:6px;margin-top:6px;">
<button id="tm_export_all" style="${saveBtnStyle()}">Export</button>
<button id="tm_import_all" style="${saveBtnStyle()}">Import</button>
</div>
</div>
<div class="tm-tab-content" data-content="api" style="padding:10px;display:none;">
<div style="margin-bottom:6px;font-size:11px;color:#aaa;line-height:1.5;">
Requires a <b style="color:white;">Public Only</b> key.<br>
Get it from: <a href="https://www.torn.com/preferences.php#tab=api"
target="_blank" style="color:#2196F3;">Settings → API Keys</a><br>
Without a key, prices fall back to RRP from the table.
</div>
<div style="display:flex;align-items:center;gap:4px;margin-bottom:12px;">
<input id="tm_api_key" type="password" value="${cfg.apiKey ?? ''}"
placeholder="Public Only key"
style="flex:1;padding:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
<button id="tm_toggle_api_key" title="Show/hide key"
style="padding:2px 6px;background:#333;color:white;border:1px solid #555;border-radius:3px;cursor:pointer;">👁</button>
</div>
<button id="tm_save_api" style="${saveBtnStyle()}">Save</button>
</div>
`;
panel.querySelectorAll('.tm-tab').forEach(tab => {
tab.addEventListener('click', () => {
panel.querySelectorAll('.tm-tab').forEach(t => {
t.style.background = '#333'; t.style.borderBottom = '1px solid #444';
});
panel.querySelectorAll('.tm-tab-content').forEach(c => c.style.display = 'none');
tab.style.background = '#222'; tab.style.borderBottom = '1px solid #222';
panel.querySelector(`.tm-tab-content[data-content="${tab.dataset.tab}"]`).style.display = 'block';
});
});
btn.onclick = () => {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
};
document.body.appendChild(btn);
document.body.appendChild(panel);
panel.querySelector('#tm_save_cfg').onclick = () => {
const c = getConfig();
c.margin = parseFloat(panel.querySelector('#tm_margin').value) || 0;
c.rounding = parseInt(panel.querySelector('#tm_rounding').value) || 1;
c.cacheTtlMin = parseInt(panel.querySelector('#tm_cache_ttl').value) || 5;
c.bulkDetection = panel.querySelector('#tm_bulk_detection').checked;
c.bulkThreshold = parseInt(panel.querySelector('#tm_bulk_threshold').value) || 200;
saveConfig(c);
alert('Saved');
};
panel.querySelector('#tm_save_api').onclick = () => {
const c = getConfig();
c.apiKey = panel.querySelector('#tm_api_key').value.trim();
saveConfig(c);
alert('Saved');
};
panel.querySelector('#tm_toggle_api_key').onclick = () => {
const inp = panel.querySelector('#tm_api_key');
inp.type = inp.type === 'password' ? 'text' : 'password';
};
panel.querySelector('#tm_export_all').onclick = () => {
panel.querySelector('#tm_io_textarea').value = exportAll();
};
panel.querySelector('#tm_import_all').onclick = () => {
importAll(panel.querySelector('#tm_io_textarea').value);
};
}
function tabBtnStyle(active) {
return `flex:1;padding:6px 4px;cursor:pointer;font-size:11px;border:none;border-radius:0;` +
`background:${active ? '#222' : '#333'};color:white;` +
`border-bottom:${active ? '1px solid #222' : '1px solid #444'};`;
}
function saveBtnStyle() {
return 'padding:3px 8px;cursor:pointer;color:white;background:#444;border:1px solid #666;border-radius:3px;';
}
/* ===============================
SCAN
=============================== */
function scanRows_Market() {
document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
.forEach(button => {
const row = button.closest('div');
if (!row) return;
const wrapper = row.parentElement;
if (!wrapper) return;
addButton_Market(wrapper);
autoApplyForSaved_Market(wrapper);
});
}
function scanRows_ViewListing() {
document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
.forEach(button => {
const row = button.closest('div');
if (!row) return;
const wrapper = row.parentElement;
if (!wrapper) return;
addButton_ViewListing(wrapper);
});
}
function updateViewListingSummary() {
const selectAllEl = document.querySelector('[class*="selectAll"]');
if (!selectAllEl) return;
let totalNet = 0;
let totalGross = 0;
let itemCount = 0;
document.querySelectorAll('[class*="itemRowWrapper"]').forEach(row => {
const priceHidden = row.querySelector('input[data-money="Infinity"][type="hidden"]');
const qtyHidden = row.querySelector('input[type="hidden"]:not([data-money="Infinity"])');
if (!priceHidden || !qtyHidden) return;
const price = Number(priceHidden.value);
const qty = Number(qtyHidden.value);
if (!price || !qty) return;
const isAnon = !!row.querySelector('[class*="checkboxWrapper"][class*="active"]');
const feeRate = isAnon ? 0.15 : 0.05;
const gross = price * qty;
const net = Math.round(gross * (1 - feeRate));
totalGross += gross;
totalNet += net;
itemCount += qty;
});
let summaryEl = document.getElementById('tm_view_summary');
if (!summaryEl) {
summaryEl = document.createElement('div');
summaryEl.id = 'tm_view_summary';
summaryEl.classList.add('tm-injected');
summaryEl.style.cssText = `
padding: 6px 10px;
font-size: 12px;
font-family: Arial, Helvetica, sans-serif;
color: #aaa;
border-top: 1px solid #333;
margin-top: 4px;
line-height: 1.7;
`;
selectAllEl.insertAdjacentElement('afterend', summaryEl);
}
const fees = totalGross - totalNet;
const tooltip = `Listed: ${fmt(itemCount)} | Gross: $${fmt(totalGross)} | Fees: -$${fmt(fees)} | Net: $${fmt(totalNet)}`;
summaryEl.title = tooltip;
summaryEl.innerHTML = `
<span style="color:#777;">Listed:</span>
<b style="color:white;">${fmt(itemCount)}</b>
<span style="color:#777;margin-left:8px;">Gross:</span>
<b style="color:white;">$${fmt(totalGross)}</b>
<span style="color:#777;margin-left:8px;">Fees:</span>
<b style="color:#F44336;">-$${fmt(fees)}</b>
<span style="color:#777;margin-left:8px;">Net:</span>
<b style="color:#8BC34A;">$${fmt(totalNet)}</b>
`;
}
function scanRows_Bazaar() {
document.querySelectorAll('li[data-group="child"]').forEach(row => {
addButton_Bazaar(row);
autoApplyForSaved_Bazaar(row);
});
}
let initialized = false;
let mutationObserver = null;
let _scanDebounceTimer = null;
function initScript() {
if (!isActivePage() || initialized) return;
initialized = true;
mutationObserver = new MutationObserver(() => {
clearTimeout(_scanDebounceTimer);
_scanDebounceTimer = setTimeout(() => {
scanRows();
if (isViewListingPage()) {
createRefreshAllWidget();
if (!document.getElementById('tm_view_summary')) updateViewListingSummary();
}
if (isManageBazaarPage()) {
createRefreshAllWidget_ManageBazaar();
}
}, 80);
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
scanRows();
if (!isConfirmPage()) createWidget();
if (isViewListingPage()) {
createRefreshAllWidget();
updateViewListingSummary();
}
if (isManageBazaarPage()) {
createRefreshAllWidget_ManageBazaar();
}
checkVersion();
}
function deactivate() {
clearTimeout(_scanDebounceTimer);
mutationObserver?.disconnect();
mutationObserver = null;
cleanup();
initialized = false;
}
initScript();
window.addEventListener('hashchange', () => {
deactivate();
if (isActivePage()) initScript();
});
})();