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