Torn Shopping List

Add items directly from the Item Market onto a shopping list, with custom buy prices that highlight on the page, including Weav3r Bazaar listings!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Torn Shopping List
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Add items directly from the Item Market onto a shopping list, with custom buy prices that highlight on the page, including Weav3r Bazaar listings!
// @author       HeyItzWerty [3626448]
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    let favorites = GM_getValue('torn_im_favorites', {});

    GM_addStyle(`
        :root { --tf-bg: rgba(34, 34, 34, 0.95); --tf-border: #444; --tf-text: #ddd; --tf-accent: #85b200; --tf-star: #ffd700; }
        
        body:not(.tf-market-active) #tf-fab, body:not(.tf-market-active) #tf-menu { display: none !important; }

        #tf-fab { display: none; position: fixed; right: 20px; bottom: 80px; width: 45px; height: 45px; background: var(--tf-bg); border: 2px solid var(--tf-accent); border-radius: 50%; cursor: pointer; align-items: center; justify-content: center; z-index: 999999; box-shadow: 0 4px 6px rgba(0,0,0,0.5); font-size: 20px; transition: transform 0.2s; }
        #tf-fab:hover { transform: scale(1.1); }
        
        #tf-menu { position: fixed; right: 10px; top: 100px; width: 260px; max-height: calc(100vh - 120px); background: var(--tf-bg); border: 1px solid var(--tf-border); border-radius: 8px; z-index: 999998; box-shadow: -2px 5px 15px rgba(0,0,0,0.7); overflow-y: auto; padding: 15px; color: var(--tf-text); font-family: 'Arial', sans-serif; backdrop-filter: blur(5px); }
        
        .tf-header { font-weight: bold; font-size: 14px; margin-bottom: 5px; text-align: center; letter-spacing: 1px;}
        .tf-author { text-align: center; font-size: 11px; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid var(--tf-border); }
        .tf-author a { color: #aaa; text-decoration: none; transition: color 0.2s; }
        .tf-author a:hover { color: var(--tf-accent); }
        
        .tf-item { display: flex; justify-content: space-between; align-items: center; background: #333; margin-bottom: 8px; padding: 8px; border-radius: 6px; border-left: 4px solid var(--tf-border); transition: background 0.2s, border-color 0.2s; }
        .tf-item.has-price { border-left-color: var(--tf-accent); }
        .tf-item:hover { background: #3a3a3a; }
        .tf-item-name { font-size: 12px; font-weight: bold; flex-grow: 1; text-decoration: none; color: white; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 10px; }
        .tf-item-name:hover { color: var(--tf-accent); }
        .tf-item-price { font-size: 11px; color: var(--tf-accent); font-weight: bold; margin-right: 10px; background: #111; padding: 3px 6px; border-radius: 4px; border: 1px solid #444; cursor: pointer; transition: color 0.2s, background 0.2s;}
        .tf-item-price:hover { background: #222; color: #fff; }
        .tf-item-price.no-price { color: #aaa; border-style: dashed; }
        .tf-btn-del { background: transparent; color: #aaa; border: none; cursor: pointer; font-size: 12px; padding: 0 5px; transition: color 0.2s; }
        .tf-btn-del:hover { color: #d9534f; }
        
        @media (max-width: 800px) { 
            body.tf-market-active #tf-fab { display: flex; }
            #tf-menu { right: 0; bottom: 0; left: 0; top: auto; width: 100%; height: 50vh; max-height: 50vh; border-radius: 15px 15px 0 0; transform: translateY(100%); transition: transform 0.3s ease; }
            #tf-menu.open { transform: translateY(0); }
        }
        
        .tf-tile-star-wrap { position: absolute; top: 4px; right: 4px; z-index: 10; background: rgba(0,0,0,0.5); border-radius: 50%; padding: 3px; display: flex; align-items: center; justify-content: center; }
        .tf-star-btn { background: transparent; border: none; font-size: 14px; cursor: pointer; transition: transform 0.2s, filter 0.2s; padding: 0; line-height: 1; filter: grayscale(100%) opacity(0.4); outline: none;}
        .tf-star-btn:hover { transform: scale(1.2); filter: grayscale(0%) drop-shadow(0 0 5px var(--tf-star)); opacity: 1; }
        .tf-star-btn.is-favorite { filter: grayscale(0%) drop-shadow(0 0 5px var(--tf-star)); opacity: 1; }
        
        .tf-deal-highlight { box-shadow: inset 0 0 15px rgba(133, 178, 0, 0.4) !important; border: 1px solid var(--tf-accent) !important; background-color: rgba(133, 178, 0, 0.15) !important; border-radius: 4px; }
    `);

    function buildUI() {
        if (document.getElementById('tf-fab')) return;

        let fab = document.createElement('div');
        fab.id = 'tf-fab';
        fab.innerHTML = '⭐';
        fab.title = "Open Shopping List";
        fab.onclick = () => document.getElementById('tf-menu').classList.toggle('open');
        document.body.appendChild(fab);

        let menu = document.createElement('div');
        menu.id = 'tf-menu';
        document.body.appendChild(menu);
        renderMenu();
    }

    function renderMenu() {
        let menu = document.getElementById('tf-menu');
        menu.innerHTML = `
            <div class="tf-header">🛒Torn Shopping List🛒</div>
            <div class="tf-author"><a href="https://www.torn.com/profiles.php?XID=3626448" target="_blank">Made by HeyItzWerty [3626448]</a></div>
        `;
        
        let keys = Object.keys(favorites);
        if(keys.length === 0) {
            menu.innerHTML += '<div style="text-align:center; font-size:12px; color:#aaa; padding: 20px 0;">Click the ⭐ on any item image to add a favorite.</div>';
            return;
        }

        keys.forEach(id => {
            let item = favorites[id];
            let row = document.createElement('div');
            let hasPrice = item.targetPrice > 0;
            row.className = 'tf-item' + (hasPrice ? ' has-price' : '');
            
            let priceText = hasPrice ? `$${item.targetPrice.toLocaleString()}` : '+ Set Price';
            let priceClass = hasPrice ? 'tf-item-price' : 'tf-item-price no-price';
            
            row.innerHTML = `
                <a href="/page.php?sid=ItemMarket#/market/view=search&itemID=${id}" class="tf-item-name" title="${item.name}">${item.name}</a>
                <span class="${priceClass}" data-id="${id}" title="Click to edit target buy price">${priceText}</span>
                <button class="tf-btn-del" data-id="${id}" title="Remove">✖</button>
            `;
            menu.appendChild(row);
        });

        menu.querySelectorAll('.tf-item-price').forEach(el => {
            el.addEventListener('click', function() {
                let idToEdit = this.getAttribute('data-id');
                let itemName = favorites[idToEdit].name;
                let currentPrice = favorites[idToEdit].targetPrice > 0 ? favorites[idToEdit].targetPrice : "";
                
                let target = prompt(`Set target BUY price for ${itemName}\n(Numbers only. Leave blank or 0 to clear):`, currentPrice);
                
                if (target !== null) {
                    let parsedPrice = parseInt(target.replace(/,/g, ''));
                    favorites[idToEdit].targetPrice = isNaN(parsedPrice) ? 0 : parsedPrice;
                    GM_setValue('torn_im_favorites', favorites);
                    
                    renderMenu();
                    forceHighlightRefresh();
                }
            });
        });

        menu.querySelectorAll('.tf-btn-del').forEach(btn => {
            btn.addEventListener('click', function() {
                let idToRemove = this.getAttribute('data-id');
                delete favorites[idToRemove];
                GM_setValue('torn_im_favorites', favorites);
                
                renderMenu();
                forceHighlightRefresh();
                
                document.querySelectorAll(`.tf-star-btn[data-item-id="${idToRemove}"]`).forEach(el => {
                    el.classList.remove('is-favorite');
                });
            });
        });
    }

    function forceHighlightRefresh() {
        document.querySelectorAll('.tf-hl-processed').forEach(el => {
            el.classList.remove('tf-hl-processed');
            el.classList.remove('tf-deal-highlight');
        });
        processDOM();
    }

    function processDOM() {
        // Star Injection into Item Tiles
        document.querySelectorAll('.itemTile___cbw7w:not(.tf-processed)').forEach(tile => {
            tile.classList.add('tf-processed');
            
            let infoBtn = tile.querySelector('button[aria-controls^="wai-itemInfo-"]');
            if(!infoBtn) return;
            
            let itemId = infoBtn.getAttribute('aria-controls').replace('wai-itemInfo-', '');
            let nameEl = tile.querySelector('.name___ukdHN');
            if(!nameEl || !itemId) return;
            
            let itemName = nameEl.innerText.trim();
            
            let starWrap = document.createElement('div');
            starWrap.className = 'tf-tile-star-wrap';
            
            let starBtn = document.createElement('button');
            starBtn.className = 'tf-star-btn' + (favorites[itemId] ? ' is-favorite' : '');
            starBtn.innerText = '⭐';
            starBtn.title = "Toggle Favorite";
            starBtn.setAttribute('data-item-id', itemId);
            
            starBtn.onclick = (e) => {
                e.stopPropagation(); e.preventDefault(); 
                
                if (favorites[itemId]) {
                    delete favorites[itemId];
                    starBtn.classList.remove('is-favorite');
                } else {
                    favorites[itemId] = { id: itemId, name: itemName, targetPrice: 0 };
                    starBtn.classList.add('is-favorite');
                }
                
                GM_setValue('torn_im_favorites', favorites);
                renderMenu();
                forceHighlightRefresh();
            };
            
            starWrap.appendChild(starBtn);
            
            let imgWrap = tile.querySelector('.imageWrapper___RqvUg');
            if(imgWrap) {
                imgWrap.style.position = 'relative'; 
                imgWrap.appendChild(starWrap);
            }
        });

        // Highlight Deals - Standard Torn Seller Rows
        document.querySelectorAll('.sellerRow___AI0m6:not(.tf-hl-processed)').forEach(row => {
            row.classList.add('tf-hl-processed');
            let priceEl = row.querySelector('.price___Uwiv2');
            let img = row.querySelector('img[src*="/images/items/"]');
            if(!priceEl || !img) return;
            
            let priceText = priceEl.innerText.replace(/[^0-9]/g, '');
            let price = parseInt(priceText);
            
            let match = img.src.match(/\/items\/(\d+)\//);
            if(!match) return;
            let itemId = match[1];
            
            if(favorites[itemId] && favorites[itemId].targetPrice > 0 && price <= favorites[itemId].targetPrice) {
                row.classList.add('tf-deal-highlight');
            }
        });

        // Highlight Deals - Weav3r Bazaar Cards (Supports both grid and list views)
        document.querySelectorAll('.bazaar-card:not(.tf-hl-processed), .bazaar-listing-card:not(.tf-hl-processed)').forEach(card => {
            card.classList.add('tf-hl-processed');
            let priceMatch = card.innerText.match(/\$([\d,]+)/);
            if(!priceMatch) return;
            let price = parseInt(priceMatch[1].replace(/,/g, ''));
            
            let container = card.closest('.bazaar-info-container');
            if(!container) return;
            let itemId = container.getAttribute('data-itemid');
            
            if(itemId && favorites[itemId] && favorites[itemId].targetPrice > 0 && price <= favorites[itemId].targetPrice) {
                card.classList.add('tf-deal-highlight');
            }
        });
    }

    function checkVisibility() {
        const url = window.location.href;
        const isMarket = url.includes('sid=ItemMarket') || url.includes('imarket.php') || url.includes('bazaar.php');
        
        if (isMarket) {
            document.body.classList.add('tf-market-active');
            processDOM();
        } else {
            document.body.classList.remove('tf-market-active');
        }
    }

    function init() {
        buildUI();
        
        let timeout;
        const observer = new MutationObserver(() => {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                checkVisibility();
            }, 150); 
        });
        
        observer.observe(document.body, { childList: true, subtree: true });
        checkVisibility();
    }

    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }
})();