Torn Bazaar Sentry

Full feature set: Price highlighting, Blacklist, Dual-tab settings, and PDA-stable links with mobile keyboard fixes.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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