// ==UserScript==
// @name Torn Display Case - Inline Diff Robust
// @namespace nova.displaycase.inline.robust
// @version 1.46
// @description Fetch display via your Torn API key and show inline diff from top item. Read-only.
// @match https://www.torn.com/displaycase.php*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect api.torn.com
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'tdc_api_key_v1';
GM_registerMenuCommand('Torn Display: Set API key', setKey);
GM_registerMenuCommand('Torn Display: Clear API key', clearKey);
function setKey() {
const cur = GM_getValue(STORAGE_KEY, '') || '';
const k = prompt('Paste your Torn API key (Display permission only):', cur);
if (k && k.trim()) {
GM_setValue(STORAGE_KEY, k.trim());
alert('API key saved locally.');
fetchAndApply();
}
}
function clearKey() {
GM_setValue(STORAGE_KEY, '');
alert('API key cleared. Reload page to remove labels.');
removeAllLabels();
}
function getKey() {
return GM_getValue(STORAGE_KEY, '') || null;
}
function showOverlay(msg, timeout = 3000) {
let o = document.getElementById('tdc-overlay');
if (!o) {
o = document.createElement('div');
o.id = 'tdc-overlay';
o.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:99999;background:#222;color:#fff;padding:8px 10px;border-radius:6px;font-size:12px;';
document.body.appendChild(o);
}
o.textContent = msg;
if (timeout) setTimeout(() => { const e = document.getElementById('tdc-overlay'); if (e) e.remove(); }, timeout);
}
function fetchDisplay(key) {
return new Promise((resolve, reject) => {
if (!key) return reject('No API key set. Use menu to set it.');
const url = `https://api.torn.com/user/?selections=display&key=${encodeURIComponent(key)}`;
GM_xmlhttpRequest({
method: 'GET',
url,
onload(res) {
try {
if (res.status !== 200) return reject(`HTTP ${res.status}`);
const json = JSON.parse(res.responseText);
if (!json || !json.display) return reject('No display data. Check API key permissions.');
resolve(json.display);
} catch (e) {
reject('Failed parsing API response: ' + (e.message || e));
}
},
onerror(err) { reject('Network error'); }
});
});
}
// Build name -> totalQuantity map from API display data
function buildQuantityMap(displayObj) {
const map = new Map();
Object.values(displayObj).forEach(it => {
if (!it) return;
const name = (it.name || (it.info && it.info.name) || String(it.id || '')).trim();
const qty = parseInt(it.quantity || 0, 10) || 0;
map.set(name, (map.get(name) || 0) + qty);
});
return map;
}
// Remove previous labels
function removeAllLabels() {
document.querySelectorAll('.tdc-inline-diff').forEach(n => n.remove());
const s = document.getElementById('tdc-summary'); if (s) s.remove();
}
// Heuristic: find candidate item card nodes
function collectItemNodes() {
const selectors = [
'.display-case-item', '.case-item', '.item', '.item-wrap', '.display-item', '.case-items li', '.case-grid > div'
];
let nodes = [];
selectors.forEach(sel => document.querySelectorAll(sel)).forEach(list => {
list.forEach(n => nodes.push(n));
});
// fallback: any element with an <img> child and text containing "x" and digits nearby
if (nodes.length === 0) {
const divs = Array.from(document.querySelectorAll('div, li'));
divs.forEach(d => {
if (d.querySelector('img')) nodes.push(d);
});
}
// unique
nodes = Array.from(new Set(nodes));
return nodes;
}
// Try to extract item name from a node
function extractNameFromNode(node) {
// common property locations
const titleSelectors = ['.title', '.item-name', '.name', '.display-name', '.item-title', 'h4', 'a'];
for (const s of titleSelectors) {
const el = node.querySelector(s);
if (el && el.textContent.trim()) return el.textContent.trim();
}
// try img alt
const img = node.querySelector('img[alt]');
if (img) {
const alt = img.getAttribute('alt').trim();
if (alt) return alt;
}
// fallback: try text content like "Camel Plushie x814"
const txt = (node.textContent || '').trim();
const m = txt.match(/([A-Za-z0-9\-\s'’,:().]+?)\s+x\s?(\d+)/);
if (m) return m[1].trim();
// last-resort: short text snippet
const words = txt.split('\n').map(s=>s.trim()).filter(Boolean);
if (words.length) return words[0].slice(0, 60).trim();
return null;
}
// Finds best matching name in map (exact then case-insensitive then substring)
function findBestMatch(name, map) {
if (!name) return null;
if (map.has(name)) return name;
const lower = name.toLowerCase();
for (const key of map.keys()) {
if (key.toLowerCase() === lower) return key;
}
for (const key of map.keys()) {
if (lower.includes(key.toLowerCase()) || key.toLowerCase().includes(lower)) return key;
}
return null;
}
// Insert inline diff into a node
function insertInline(node, diff) {
// avoid duplication
if (node.querySelector('.tdc-inline-diff')) return;
const wrap = document.createElement('div');
wrap.className = 'tdc-inline-diff';
wrap.textContent = (diff > 0 ? '+' : '') + diff;
wrap.style.cssText = `
font-size:11px;
color:${diff === 0 ? '#aaa' : diff < 0 ? '#ff6b6b' : '#8bc34a'};
text-align:center;
margin-top:4px;
pointer-events:none;
`;
// try to append to a small info area if exists
const infoSelectors = ['.item-description', '.description', '.item-info', '.info', '.case-item-info', '.meta'];
for (const s of infoSelectors) {
const el = node.querySelector(s);
if (el) { el.appendChild(wrap); return; }
}
// otherwise append at end of node
node.appendChild(wrap);
}
// Insert top summary near the title (optional)
function insertSummary(topNames, qty) {
if (document.getElementById('tdc-summary')) return;
const s = document.createElement('div');
s.id = 'tdc-summary';
s.textContent = `Highest: ${topNames.join(', ')} (${qty})`;
s.style.cssText = 'color:#4caf50;font-size:13px;font-weight:600;margin:8px 0;text-align:center;';
const title = document.querySelector('.title-black, .content-title, h4, .title');
if (title && title.parentNode) title.after(s);
else document.body.prepend(s);
}
// Main application: match nodes to API map and inject labels
function applyDisplayMap(map) {
removeAllLabels();
if (!map || map.size === 0) { showOverlay('No display items from API', 3000); return; }
// compute max qty
const maxQty = Math.max(...Array.from(map.values()));
const topNames = [...map.entries()].filter(([_, q]) => q === maxQty).map(([n]) => n);
insertSummary(topNames, maxQty);
const nodes = collectItemNodes();
if (!nodes || nodes.length === 0) {
showOverlay('No item nodes found in DOM', 3000);
return;
}
nodes.forEach(node => {
try {
const name = extractNameFromNode(node);
const best = findBestMatch(name, map);
if (!best) return;
const qty = map.get(best) || 0;
const diff = qty - maxQty;
insertInline(node, diff);
} catch (e) {
// ignore per-node errors
}
});
}
// Fetch + apply flow
async function fetchAndApply() {
const key = getKey();
if (!key) { showOverlay('No API key. Use menu to set.', 4000); return; }
showOverlay('Fetching display data...');
try {
const display = await fetchDisplay(key);
const qmap = buildQuantityMap(display);
applyDisplayMap(qmap);
showOverlay('Display diffs applied', 1500);
} catch (err) {
console.error('TDC error', err);
showOverlay('Error: ' + String(err), 5000);
}
}
// Observe page changes and re-run when display case appears or changes
const pageObserver = new MutationObserver((mut) => {
if (window._tdc_debounce) clearTimeout(window._tdc_debounce);
window._tdc_debounce = setTimeout(() => {
// only trigger on displaycase page or if display-like container exists
if (location.pathname.includes('displaycase.php') || document.querySelector('.display-items, #displayCaseItems, .display-case')) {
fetchAndApply();
}
}, 500);
});
pageObserver.observe(document.documentElement, { childList: true, subtree: true });
// initial attempt
setTimeout(fetchAndApply, 1000);
})();