Full feature set: Price highlighting, Blacklist, Dual-tab settings, and PDA-stable links with mobile keyboard fixes.
// ==UserScript==
// @name Torn Bazaar Sentry
// @namespace torn.bazaar_sentry
// @version 2.2.1
// @description Full feature set: Price highlighting, Blacklist, Dual-tab settings, and PDA-stable links with mobile keyboard fixes.
// @author BBSmalls [3908857]
// @match *://www.torn.com/bazaar.php*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// --- DATA & STORAGE ---
const defaultPrices = [{ amt: 1, enabled: true, color: "#2ecc71" }, { amt: 2, enabled: true, color: "#2ecc71" }];
const defaultIgnored = [{ name: "Baseball Bat", enabled: false }];
let userSettings = JSON.parse(localStorage.getItem('bbsmalls_sentry_list_v2')) || defaultPrices;
let rawIgnored = JSON.parse(localStorage.getItem('bbsmalls_sentry_ignored_v1')) || defaultIgnored;
let ignoredItems = rawIgnored.map(item => (typeof item === 'string' ? { name: item, enabled: true } : item));
const savePrices = () => localStorage.setItem('bbsmalls_sentry_list_v2', JSON.stringify(userSettings));
const saveIgnored = () => localStorage.setItem('bbsmalls_sentry_ignored_v1', JSON.stringify(ignoredItems));
const hexToRgba = (hex, alpha = 0.25) => {
const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
let settingsPanel = null;
let currentTab = 'prices';
let bannerDismissed = false;
// --- SHADOW DOM BANNER ---
let host = document.getElementById('sentry-banner-host');
if (!host) {
host = document.createElement('div');
host.id = 'sentry-banner-host';
document.documentElement.appendChild(host);
host.attachShadow({mode: 'open'});
}
const updateFloatingBanner = (isFound) => {
const root = host.shadowRoot;
if (!isFound) { bannerDismissed = false; root.innerHTML = ''; return; }
if (bannerDismissed || root.innerHTML !== '') return;
root.innerHTML = `<style>.w{position:fixed;top:0;left:0;width:100%;background:#e74c3c;color:white;display:flex;align-items:center;justify-content:center;padding:12px 0;z-index:2147483647;box-shadow:0 2px 10px rgba(0,0,0,0.5);pointer-events:none;font-family:Arial;font-weight:bold;}.t{font-size:16px;text-transform:uppercase;letter-spacing:2px;}.b{position:absolute;right:20px;cursor:pointer;font-size:24px;pointer-events:auto;padding:0 10px;}</style><div class="w"><span class="t">⚠️ Item(s) Detected ⚠️</span><span class="b" id="c">×</span></div>`;
root.getElementById('c').onclick = () => { bannerDismissed = true; root.innerHTML = ''; };
};
// --- MODAL ENGINE (Mobile Keyboard Optimized) ---
const showModal = (title, contentHTML, onConfirm) => {
const overlay = document.createElement('div');
overlay.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:2000000;display:flex;align-items:center;justify-content:center;font-family:Arial,sans-serif;`;
const modal = document.createElement('div');
modal.style.cssText = `background:#222;border:3px solid #666;border-radius:8px;width:200px;overflow:hidden;box-shadow:0 0 30px rgba(0,0,0,0.5);`;
modal.innerHTML = `
<div style="background:#333;padding:12px;font-weight:bold;font-size:14px;color:#fff;text-align:center;border-bottom:1px solid #444;">${title}</div>
<div style="padding:15px;color:#ddd;font-size:13px;text-align:center;">
${contentHTML}
<div id="modal-error" style="color:#e74c3c;font-size:11px;margin-top:8px;font-weight:bold;min-height:14px;"></div>
</div>
<div style="display:flex;border-top:1px solid #444;">
<button id="m-cancel" style="flex:1;padding:10px;background:#333;color:#aaa;border:none;cursor:pointer;pointer-events:auto;">Cancel</button>
<button id="m-ok" style="flex:1;padding:12px;background:#27ae60;color:#fff;border:none;cursor:pointer;font-weight:bold;border-left:1px solid #444;pointer-events:auto;">OK</button>
</div>`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const errorEl = modal.querySelector('#modal-error'),
input = modal.querySelector('input'),
btnOk = modal.querySelector('#m-ok'),
btnCancel = modal.querySelector('#m-cancel');
const handleConfirm = (e) => {
if (e) { e.preventDefault(); e.stopPropagation(); }
if (input) input.blur(); // Dismisses mobile keyboard
const setError = (msg) => {
errorEl.innerText = msg;
modal.animate([{transform:'translateX(-5px)'},{transform:'translateX(5px)'}],{duration:100,iterations:2});
};
if (onConfirm(input ? input.value : true, setError) !== false) overlay.remove();
};
if (input) {
setTimeout(() => input.focus(), 10);
input.onkeydown = (e) => { if (e.key === 'Enter') handleConfirm(e); else errorEl.innerText = ""; };
}
btnOk.addEventListener('click', handleConfirm);
btnOk.addEventListener('touchstart', handleConfirm, {passive: false});
btnCancel.onclick = () => overlay.remove();
btnCancel.addEventListener('touchstart', (e) => { e.preventDefault(); overlay.remove(); }, {passive: false});
};
// --- SETTINGS PANEL ---
const createSettingsPanel = () => {
if (settingsPanel) settingsPanel.remove();
userSettings.sort((a,b)=>a.amt-b.amt);
ignoredItems.sort((a,b)=>a.name.localeCompare(b.name));
settingsPanel = document.createElement('div');
settingsPanel.style.cssText = `position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#222;color:white;border-radius:8px;z-index:1000001;width:225px;font-family:Arial,sans-serif;border:3px solid #666;display:none;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.6);`;
const tabs = `<div style="display:flex;background:#333;border-bottom:1px solid #444;"><div id="t-p" style="flex:1;padding:10px;text-align:center;cursor:pointer;font-size:12px;font-weight:bold;${currentTab==='prices'?'background:#222;border-bottom:2px solid #27ae60;':''}">Prices</div><div id="t-i" style="flex:1;padding:10px;text-align:center;cursor:pointer;font-size:12px;font-weight:bold;${currentTab==='ignored'?'background:#222;border-bottom:2px solid #27ae60;':''}">Blacklist</div></div>`;
const ROW = `margin-bottom:6px;display:flex;align-items:center;gap:12px;background:#333;padding:0 10px;border-radius:4px;height:42px;box-sizing:border-box;`;
const LBL = `font-size:13px;flex-grow:1;overflow:hidden;white-space:nowrap;font-weight:500;line-height:42px;cursor:pointer;`;
const DEL = `cursor:pointer;color:#e74c3c;font-weight:bold;font-size:22px;line-height:42px;width:20px;text-align:center;`;
let html = (currentTab==='prices')
? userSettings.map((item,i)=>`<div style="${ROW}"><span class="p-d" data-i="${i}" style="${DEL}">×</span><input type="checkbox" class="p-t" data-i="${i}" ${item.enabled?'checked':''} style="width:20px;height:20px;"><input type="color" class="p-c" data-i="${i}" value="${item.color}" style="width:24px;height:24px;border:none;background:none;padding:0;"><label class="p-e" data-i="${i}" style="${LBL}">$${item.amt.toLocaleString()}</label></div>`).join('')
: ignoredItems.map((item,i)=>`<div style="${ROW}"><span class="i-d" data-i="${i}" style="${DEL}">×</span><input type="checkbox" class="i-t" data-i="${i}" ${item.enabled?'checked':''} style="width:20px;height:20px;"><label class="i-e" data-i="${i}" style="${LBL}">${item.name}</label></div>`).join('');
settingsPanel.innerHTML = `<div style="background:#444;padding:10px;text-align:center;font-weight:bold;font-size:13px;">Bazaar Sentry</div>${tabs}<div style="padding:12px;"><div style="height:205px;overflow-y:auto;">${html}</div><div style="display:flex;gap:8px;margin-top:10px;"><button id="add-b" style="padding:4px 12px;background:#27ae60;color:white;border:none;cursor:pointer;font-size:18px;border-radius:4px;">+</button><button id="close-b" style="flex-grow:1;background:#34495e;color:white;border:none;cursor:pointer;font-weight:bold;border-radius:4px;">Close</button></div></div>`;
document.body.appendChild(settingsPanel);
document.getElementById('t-p').onclick = () => { currentTab='prices'; createSettingsPanel(); settingsPanel.style.display='block'; };
document.getElementById('t-i').onclick = () => { currentTab='ignored'; createSettingsPanel(); settingsPanel.style.display='block'; };
document.getElementById('close-b').onclick = () => settingsPanel.style.display='none';
if(currentTab==='prices'){
settingsPanel.querySelectorAll('.p-t').forEach(el=>el.onchange=(e)=>{userSettings[e.target.dataset.i].enabled=e.target.checked;savePrices();});
settingsPanel.querySelectorAll('.p-c').forEach(el=>el.onchange=(e)=>{userSettings[e.target.dataset.i].color=e.target.value;savePrices();});
settingsPanel.querySelectorAll('.p-d').forEach(el=>el.onclick=(e)=>{const idx=e.target.dataset.i;showModal("Delete",`Remove $${userSettings[idx].amt.toLocaleString()}?`,()=>{userSettings.splice(idx,1);savePrices();createSettingsPanel();settingsPanel.style.display='block';});});
settingsPanel.querySelectorAll('.p-e').forEach(el=>el.onclick=(e)=>{const idx=e.target.dataset.i;showModal("Edit Price",`<input type="text" value="${userSettings[idx].amt}" style="width:100px;padding:5px;text-align:center;">`,(v,err)=>{const p=parseInt(v.replace(/[^0-9]/g,'')); if(!p){err("Invalid!");return false;} if(userSettings.some((it,n)=>it.amt===p && n!=idx)){err("Duplicate Entry!");return false;} userSettings[idx].amt=p;savePrices();createSettingsPanel();settingsPanel.style.display='block';});});
} else {
settingsPanel.querySelectorAll('.i-t').forEach(el=>el.onchange=(e)=>{ignoredItems[e.target.dataset.i].enabled=e.target.checked;saveIgnored();});
settingsPanel.querySelectorAll('.i-d').forEach(el=>el.onclick=(e)=>{const idx=e.target.dataset.i;showModal("Delete",`Remove "${ignoredItems[idx].name}"?`,()=>{ignoredItems.splice(idx,1);saveIgnored();createSettingsPanel();settingsPanel.style.display='block';});});
settingsPanel.querySelectorAll('.i-e').forEach(el=>el.onclick=(e)=>{const idx=e.target.dataset.i;showModal("Edit Name",`<input type="text" value="${ignoredItems[idx].name}" style="width:150px;padding:5px;text-align:center;">`,(v,err)=>{const n=v.trim(); if(!n){err("Empty!");return false;} if(ignoredItems.some((it,m)=>it.name.toLowerCase()===n.toLowerCase() && m!=idx)){err("Duplicate Entry!");return false;} ignoredItems[idx].name=n;saveIgnored();createSettingsPanel();settingsPanel.style.display='block';});});
}
document.getElementById('add-b').onclick=()=>{
if(currentTab==='prices'){ showModal("Add Price",`<input type="text" placeholder="$0" style="width:100px;padding:5px;text-align:center;">`,(v,err)=>{const p=parseInt(v.replace(/[^0-9]/g,'')); if(!p){err("Invalid!");return false;} if(userSettings.some(it=>it.amt===p)){err("Duplicate Entry!");return false;} userSettings.push({amt:p,enabled:true,color:"#2ecc71"});savePrices();createSettingsPanel();settingsPanel.style.display='block';});}
else { showModal("Blacklist",`<input type="text" placeholder="Item Name" style="width:150px;padding:5px;text-align:center;">`,(v,err)=>{const n=v.trim(); if(!n){err("Empty!");return false;} if(ignoredItems.some(it=>it.name.toLowerCase()===n.toLowerCase())){err("Duplicate Entry!");return false;} ignoredItems.push({name:n,enabled:true});saveIgnored();createSettingsPanel();settingsPanel.style.display='block';});}
};
};
// --- STABLE PDA LINK ---
const injectSettingsLink = () => {
if (document.getElementById('sentry-settings-link')) return;
const target = document.querySelector('div[class*="titleContainer"]');
if (target) {
const l = document.createElement('div');
l.id = 'sentry-settings-link';
l.style.cssText = `margin-left:15px;cursor:pointer;color:#b0b0b0;font-size:13px;display:inline-flex;align-items:center;gap:4px;z-index:999999;pointer-events:auto !important;`;
l.innerHTML = `🔥<span style="text-decoration:underline;">Bazaar Sentry</span>`;
l.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if(settingsPanel) settingsPanel.style.display='block'; }, true);
target.appendChild(l);
}
};
// --- DETECTION ENGINE ---
const highlightItems = () => {
injectSettingsLink();
let anyMatch = false;
const activeBlacklist = ignoredItems.filter(i => i.enabled).map(i => i.name.toLowerCase());
document.querySelectorAll('[data-testid="item"]').forEach(card => {
const pDiv = card.querySelector('[data-testid="price"]'), nEl = card.querySelector('[data-testid="name"]');
const btn = card.querySelector('[data-testid="buy-button"]') || card.querySelector('[data-testid="activate-buy-button"]');
if (pDiv && nEl && btn) {
const rEl = pDiv.querySelector('[class*="rates"]'), rTxt = rEl ? rEl.innerText : "";
const curP = parseInt(pDiv.innerText.replace(rTxt, "").replace(/[^0-9]/g, ''), 10);
const m = userSettings.find(it => it.amt === curP && it.enabled);
const isLocked = btn.hasAttribute('disabled') || btn.disabled || btn.classList.contains('disabled');
if (m && !isLocked && !activeBlacklist.includes(nEl.innerText.trim().toLowerCase())) {
card.style.setProperty('background-color', hexToRgba(m.color), 'important');
card.style.setProperty('box-shadow', `inset 0 0 0 4px ${m.color}`, 'important');
anyMatch = true;
} else {
card.style.removeProperty('background-color');
card.style.removeProperty('box-shadow');
}
}
});
updateFloatingBanner(anyMatch);
};
const init = () => {
createSettingsPanel();
if (!localStorage.getItem('bbsmalls_sentry_initialized')) {
if (settingsPanel) settingsPanel.style.display = 'block';
localStorage.setItem('bbsmalls_sentry_initialized', 'true');
}
const observer = new MutationObserver(highlightItems);
observer.observe(document.body, { childList: true, subtree: true });
highlightItems();
};
init();
})();