Torn Inventory Management

Manage your Torn inventory with custom categories

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Inventory Management
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  Manage your Torn inventory with custom categories
// @author       TornUser
// @match        https://www.torn.com/item.php*
// @match        https://www.torn.com/index.php?page=items*
// @match        https://www.torn.com/bazaar.php*
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const STORAGE_KEY = 'torn_inventory_management_categories';
    const ITEMS_KEY = 'torn_inventory_management_items_mapping';
    
    // Global variables
    let categories = {};
    let itemsMapping = {};
    let isInitialized = false;

    // Initialize the script
    function init() {
        if (isInitialized) return;
        
        // Load data first
        loadData();
        
        // Check which page we're on and initialize accordingly
        if (isInventoryPage()) {
            console.log('[Torn Inventory] Initializing inventory management...');
            createCategoryInterface();
            
            setTimeout(() => {
                console.log('[Torn Inventory] Looking for inventory items...');
                setupInventoryObserver();
                addInventoryControlsToItems();
            }, 1000);
            
        } else if (isBazaarPage()) {
            console.log('[Torn Inventory] Initializing bazaar category display...');
            setTimeout(() => {
                showCategoriesOnBazaarPage();
                setupBazaarObserver();
            }, 2000);
            
        } else if (isItemMarketPage()) {
            console.log('[Torn Inventory] Initializing item market category display...');
            setTimeout(() => {
                showCategoriesOnItemMarketPage();
                setupItemMarketObserver();
            }, 2000);
            
        } else {
            console.log('[Torn Inventory] Not on supported page, skipping initialization');
            return;
        }
        
        isInitialized = true;
        console.log('[Torn Inventory] Inventory Management loaded successfully');
    }

    // Check if current page is inventory
    function isInventoryPage() {
        return (window.location.href.includes('item.php') || 
               window.location.href.includes('page=items')) &&
               (document.querySelector('.items-wrap, .inventory-wrap, #inventory, .item-list') !== null);
    }

    // Check if current page is bazaar
    function isBazaarPage() {
        return window.location.href.includes('bazaar.php');
    }

    // Check if current page is item market
    function isItemMarketPage() {
        return window.location.href.includes('page.php?sid=ItemMarket');
    }

    // Load saved data from storage
    function loadData() {
        try {
            const savedCategories = GM_getValue(STORAGE_KEY, '{}');
            const savedItems = GM_getValue(ITEMS_KEY, '{}');
            
            categories = JSON.parse(savedCategories);
            itemsMapping = JSON.parse(savedItems);
            
            console.log('[Torn Inventory] Loaded data:', {
                categories: Object.keys(categories).length,
                items: Object.keys(itemsMapping).length
            });
            
            // Initialize with default category if empty
            if (Object.keys(categories).length === 0) {
                categories = {
                    'default': {
                        id: 'default',
                        name: 'Uncategorized',
                        parent: null,
                        children: [],
                        collapsed: false,
                        order: 0
                    }
                };
                saveData();
            }
            
            // Add order property to existing categories if missing
            Object.values(categories).forEach((category, index) => {
                if (category.order === undefined) {
                    category.order = index;
                }
            });
            
        } catch (error) {
            console.error('[Torn Inventory] Error loading data:', error);
            categories = {
                'default': {
                    id: 'default',
                    name: 'Uncategorized',
                    parent: null,
                    children: [],
                    collapsed: false,
                    order: 0
                }
            };
            itemsMapping = {};
        }
    }

    // Save data to storage
    function saveData() {
        try {
            GM_setValue(STORAGE_KEY, JSON.stringify(categories));
            GM_setValue(ITEMS_KEY, JSON.stringify(itemsMapping));
            console.log('[Torn Inventory] Data saved successfully');
        } catch (error) {
            console.error('[Torn Inventory] Error saving data:', error);
        }
    }

    // Show categories on bazaar page
    function showCategoriesOnBazaarPage() {
        console.log('[Torn Inventory] Adding category labels to bazaar page...');
        
        setTimeout(() => {
            const itemElements = findItemElementsOnPage();
            console.log('[Torn Inventory] Found ' + itemElements.length + ' items on bazaar page');
            
            itemElements.forEach(element => {
                const itemId = extractItemIdFromElement(element);
                const itemName = extractItemNameFromElement(element);
                
                if (itemId && itemsMapping[itemId]) {
                    const categoryId = itemsMapping[itemId];
                    const category = categories[categoryId];
                    if (category) {
                        addCategoryLabelToElement(element, category);
                        console.log('[Torn Inventory] Added category label "' + category.name + '" to ' + (itemName || itemId));
                    }
                }
            });
        }, 500);
    }

    // Show categories on item market page
    function showCategoriesOnItemMarketPage() {
        console.log('[Torn Inventory] Adding category labels to item market page...');
        
        setTimeout(() => {
            const itemElements = findItemElementsOnPage();
            console.log('[Torn Inventory] Found ' + itemElements.length + ' items on market page');
            
            itemElements.forEach(element => {
                const itemId = extractItemIdFromElement(element);
                const itemName = extractItemNameFromElement(element);
                
                if (itemId && itemsMapping[itemId]) {
                    const categoryId = itemsMapping[itemId];
                    const category = categories[categoryId];
                    if (category) {
                        addCategoryLabelToElement(element, category);
                        console.log('[Torn Inventory] Added category label "' + category.name + '" to ' + (itemName || itemId));
                    }
                }
            });
        }, 500);
    }

    // Find item elements on any page
    function findItemElementsOnPage() {
        const selectors = [
            '[data-item]',
            'li[data-item]',
            'div[data-item]',
            'tr[data-item]'
        ];
        
        const foundElements = [];
        
        selectors.forEach(selector => {
            try {
                const elements = document.querySelectorAll(selector);
                elements.forEach(element => {
                    if (looksLikeItemElement(element) && !foundElements.includes(element)) {
                        foundElements.push(element);
                    }
                });
            } catch (e) {
                // Ignore selector errors
            }
        });
        
        return foundElements;
    }

    // Check if an element looks like it represents an item
    function looksLikeItemElement(element) {
        if (element.querySelector('.category-label')) {
            return false;
        }
        
        const hasContent = element.textContent.trim().length > 2;
        if (!hasContent) return false;
        
        const isVisible = element.offsetWidth > 0 && element.offsetHeight > 0;
        if (!isVisible) return false;
        
        const hasReasonableSize = element.offsetWidth > 50 && element.offsetHeight > 20;
        if (!hasReasonableSize) return false;
        
        const hasImage = element.querySelector('img');
        const hasDataItem = element.hasAttribute('data-item');
        const hasCheckbox = element.querySelector('input[type="checkbox"]');
        
        return hasImage || hasDataItem || hasCheckbox;
    }

    // Extract item ID from an element on any page
    function extractItemIdFromElement(element) {
        let itemId = element.getAttribute('data-item') || 
                     element.getAttribute('data-id') ||
                     element.getAttribute('data-item-id');
        
        if (!itemId) {
            const childWithId = element.querySelector('[data-item], [data-id], [data-item-id]');
            if (childWithId) {
                itemId = childWithId.getAttribute('data-item') || 
                         childWithId.getAttribute('data-id') || 
                         childWithId.getAttribute('data-item-id');
            }
        }
        
        if (!itemId) {
            let parent = element.parentElement;
            let levels = 0;
            while (parent && levels < 2) {
                itemId = parent.getAttribute('data-item') || 
                         parent.getAttribute('data-id') || 
                         parent.getAttribute('data-item-id');
                if (itemId) break;
                parent = parent.parentElement;
                levels++;
            }
        }
        
        return itemId;
    }

    // Extract item name from an element on any page
    function extractItemNameFromElement(element) {
        const textElements = element.querySelectorAll('div, span, td, th, p');
        
        for (const textEl of textElements) {
            const text = textEl.textContent.trim();
            
            if (text && 
                text.length > 2 && 
                text.length < 100 &&
                !text.match(/^\d+$/) &&
                !text.match(/^[\d,.\s$rrp]+$/i) &&
                !text.includes('$') &&
                !text.includes('RRP') &&
                !text.toLowerCase().includes('qty') &&
                !text.toLowerCase().includes('price') &&
                !text.toLowerCase().includes('equipped') &&
                !text.toLowerCase().includes('untradeable')) {
                
                if (textEl.children.length === 0) {
                    return text;
                }
            }
        }
        
        const img = element.querySelector('img[alt]');
        if (img && img.alt) {
            return img.alt;
        }
        
        return null;
    }

    // Add a category label to an element
    function addCategoryLabelToElement(element, category) {
        if (element.querySelector('.category-label')) {
            return;
        }
        
        const label = document.createElement('div');
        label.className = 'category-label';
        if (category.parent) {
            label.classList.add('subcategory');
        }
        
        let categoryText = category.name;
        if (category.parent && categories[category.parent]) {
            categoryText = categories[category.parent].name + ' → ' + category.name;
        }
        
        label.textContent = categoryText;
        label.title = 'Category: ' + categoryText;
        
        const insertionPoint = findBestInsertionPoint(element);
        if (insertionPoint) {
            insertionPoint.appendChild(label);
        } else {
            element.style.position = 'relative';
            label.style.position = 'absolute';
            label.style.top = '2px';
            label.style.right = '2px';
            label.style.zIndex = '100';
            element.appendChild(label);
        }
    }

    // Find the best place to insert a category label
    function findBestInsertionPoint(element) {
        const candidates = element.querySelectorAll('td:last-child, div:last-child, div, span, td');
        
        for (const candidate of candidates) {
            const text = candidate.textContent.trim().toLowerCase();
            
            if (text === '' || 
                text === 'equipped' || 
                text === 'untradeable' ||
                text.includes('qty') ||
                text.includes('price') ||
                candidate.children.length === 0) {
                return candidate;
            }
        }
        
        const containerDivs = element.querySelectorAll('div');
        if (containerDivs.length > 0) {
            return containerDivs[containerDivs.length - 1];
        }
        
        return null;
    }

    // Setup observer for bazaar page
    function setupBazaarObserver() {
        const observer = new MutationObserver(() => {
            setTimeout(showCategoriesOnBazaarPage, 1000);
        });
        
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        
        console.log('[Torn Inventory] Bazaar observer set up');
    }

    // Setup observer for item market page
    function setupItemMarketObserver() {
        const observer = new MutationObserver(() => {
            setTimeout(showCategoriesOnItemMarketPage, 1000);
        });
        
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        
        console.log('[Torn Inventory] Item market observer set up');
    }

    // Create the category interface
    function createCategoryInterface() {
        const inventoryContainer = findInventoryContainer();
        if (!inventoryContainer) {
            console.warn('[Torn Inventory] Inventory container not found');
            return;
        }

        const categoriesPanel = createCategoriesPanel();
        inventoryContainer.parentNode.insertBefore(categoriesPanel, inventoryContainer);
        renderCategories();
    }

    // Find the inventory container
    function findInventoryContainer() {
        console.log('[Torn Inventory] Searching for inventory container...');
        
        const selectors = [
            '.items-wrap',
            '.inventory-wrap', 
            '#inventory',
            '.item-list',
            '.items-cont',
            '.item-list-wrap',
            '.your-items'
        ];
        
        for (const selector of selectors) {
            const element = document.querySelector(selector);
            if (element) {
                console.log('[Torn Inventory] Found inventory container with selector:', selector);
                return element;
            }
        }
        
        const allItems = document.querySelector('ul.all-items');
        if (allItems) {
            console.log('[Torn Inventory] Found ul.all-items container');
            return allItems.parentElement || allItems;
        }
        
        console.warn('[Torn Inventory] No inventory container found');
        return null;
    }

    // Create the categories panel
    function createCategoriesPanel() {
        const panel = document.createElement('div');
        panel.id = 'torn-inventory-management-panel';
        panel.innerHTML = '<div class="inventory-management-header"><h3>Inventory Categories</h3><div class="inventory-management-controls"><button id="add-category-btn" class="torn-btn">+ Add Category</button><button id="reset-categories-btn" class="torn-btn" style="background: #d32f2f;">Reset All</button><button id="toggle-categories-btn" class="torn-btn">Toggle</button></div></div><div id="categories-container" class="categories-container"></div>';
        
        addStyles();
        
        panel.querySelector('#add-category-btn').addEventListener('click', () => {
            showAddCategoryDialog();
        });
        panel.querySelector('#reset-categories-btn').addEventListener('click', resetAllCategories);
        panel.querySelector('#toggle-categories-btn').addEventListener('click', toggleCategoriesPanel);
        
        return panel;
    }

    // Add CSS styles
    function addStyles() {
        const styles = '#torn-inventory-management-panel { background: #2e2e2e; border: 1px solid #444; border-radius: 5px; margin: 10px 0; padding: 15px; color: #ddd; } .inventory-management-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #444; padding-bottom: 10px; } .inventory-management-header h3 { margin: 0; color: #fff; } .inventory-management-controls { display: flex; gap: 10px; } .torn-btn { background: #4a4a4a; border: 1px solid #666; color: #ddd; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; } .torn-btn:hover { background: #555; } .categories-container { max-height: 300px; overflow-y: auto; } .category-item { background: #3a3a3a; border: 1px solid #555; border-radius: 3px; margin: 5px 0; padding: 10px; position: relative; } .category-item.collapsed .category-children, .category-item.collapsed .category-items { display: none; } .category-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; } .category-name { font-weight: bold; color: #fff; } .category-controls { display: flex; gap: 5px; } .category-controls button { background: #555; border: none; color: #ddd; padding: 2px 6px; border-radius: 2px; cursor: pointer; font-size: 10px; } .category-controls button:hover { background: #666; } .category-reorder-controls { display: flex; gap: 2px; margin-right: 5px; } .reorder-btn { background: #666; border: none; color: #ddd; padding: 1px 4px; border-radius: 2px; cursor: pointer; font-size: 10px; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; } .reorder-btn:hover { background: #777; } .reorder-btn:disabled { background: #444; color: #666; cursor: not-allowed; } .category-children { margin-left: 20px; margin-top: 10px; } .torn-inventory-control { margin: 5px 0; padding: 3px; background: rgba(0, 0, 0, 0.3); border-radius: 3px; display: flex; gap: 5px; align-items: center; } .category-selector { background: #4a4a4a; border: 1px solid #666; color: #ddd; padding: 2px 4px; border-radius: 2px; font-size: 11px; flex: 1; max-width: 150px; } .category-quick-btn { background: #555; border: 1px solid #666; color: #ddd; padding: 2px 6px; border-radius: 2px; cursor: pointer; font-size: 10px; } .category-quick-btn:hover { background: #666; } .torn-quick-category-menu { background: #2e2e2e; border: 1px solid #444; border-radius: 3px; padding: 5px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); min-width: 120px; } .quick-menu-title { font-size: 11px; font-weight: bold; color: #fff; padding: 3px 0; border-bottom: 1px solid #444; margin-bottom: 3px; } .quick-category-btn { display: block; width: 100%; background: #4a4a4a; border: 1px solid #666; color: #ddd; padding: 4px 8px; margin: 2px 0; border-radius: 2px; cursor: pointer; font-size: 11px; text-align: left; } .quick-category-btn:hover { background: #555; } .quick-category-btn.remove-btn { background: #d32f2f; border-color: #f44336; } .quick-category-btn.remove-btn:hover { background: #f44336; } .quick-category-btn.close-btn { background: #666; margin-top: 5px; border-top: 1px solid #777; } .category-items { margin-top: 10px; } .category-item-preview { background: #4a4a4a; border: 1px solid #666; padding: 5px; margin: 2px 0; border-radius: 2px; font-size: 11px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: background-color 0.2s ease; } .category-item-preview:hover { background: #555; } @keyframes greenFlash { 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); } 50% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0.3); } 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); } } .inventory-item-highlighted { animation: greenFlash 2s ease-out; border: 2px solid #4CAF50 !important; background: rgba(76, 175, 80, 0.1) !important; } .remove-item-btn { background: #d32f2f; border: none; color: white; padding: 1px 4px; border-radius: 2px; cursor: pointer; font-size: 10px; } .remove-item-btn:hover { background: #f44336; } .category-label { background: #4a4a4a; color: #ddd; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-style: italic; margin: 2px 0; display: inline-block; border: 1px solid #666; } .category-label.subcategory { background: #5a5a5a; border-color: #777; } .category-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; } .category-modal-content { background: #2e2e2e; border: 1px solid #444; border-radius: 5px; padding: 20px; max-width: 400px; width: 90%; color: #ddd; } .category-modal input, .category-modal select { width: 100%; padding: 8px; margin: 10px 0; background: #4a4a4a; border: 1px solid #666; border-radius: 3px; color: #ddd; } .category-modal-buttons { display: flex; gap: 10px; justify-content: flex-end; margin-top: 15px; }';
        
        const styleSheet = document.createElement('style');
        styleSheet.textContent = styles;
        document.head.appendChild(styleSheet);
    }

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
    
    setTimeout(init, 1000);

})();