Torn Inventory Categories

Create custom categories for your Torn inventory with drag and drop functionality

当前为 2025-07-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         Torn Inventory Categories
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Create custom categories for your Torn inventory with drag and drop functionality
// @author       TornUser
// @match        https://www.torn.com/item.php*
// @match        https://www.torn.com/index.php?page=items*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const STORAGE_KEY = 'torn_inventory_categories';
    const ITEMS_KEY = 'torn_inventory_items_mapping';
    
    // Global variables
    let categories = {};
    let itemsMapping = {};
    let draggedItem = null;
    let isInitialized = false;

    // Initialize the script
    function init() {
        if (isInitialized) return;
        
        // Check if we're on the correct page
        if (!isInventoryPage()) return;
        
        loadData();
        createCategoryInterface();
        setupInventoryObserver();
        isInitialized = true;
        
        console.log('[Torn Categories] Inventory Categories 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') !== null;
    }

    // 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);
            
            // Initialize with default category if empty
            if (Object.keys(categories).length === 0) {
                categories = {
                    'default': {
                        id: 'default',
                        name: 'Uncategorized',
                        parent: null,
                        children: [],
                        collapsed: false
                    }
                };
                saveData();
            }
        } catch (error) {
            console.error('[Torn Categories] Error loading data:', error);
            categories = {
                'default': {
                    id: 'default',
                    name: 'Uncategorized',
                    parent: null,
                    children: [],
                    collapsed: false
                }
            };
            itemsMapping = {};
        }
    }

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

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

        // Create categories panel
        const categoriesPanel = createCategoriesPanel();
        
        // Insert categories panel before inventory
        inventoryContainer.parentNode.insertBefore(categoriesPanel, inventoryContainer);
        
        // Modify inventory items to be draggable
        makeItemsDraggable();
        
        // Render categories
        renderCategories();
    }

    // Find the inventory container
    function findInventoryContainer() {
        // Try multiple selectors for different inventory layouts
        const selectors = [
            '.items-wrap',
            '.inventory-wrap',
            '#inventory',
            '.item-list',
            '.items-cont'
        ];
        
        for (const selector of selectors) {
            const element = document.querySelector(selector);
            if (element) return element;
        }
        
        // Fallback: look for elements containing item classes
        const itemElements = document.querySelectorAll('[class*="item"]');
        if (itemElements.length > 0) {
            return itemElements[0].closest('.content-wrapper, .main-content, body');
        }
        
        return null;
    }

    // Create the categories panel
    function createCategoriesPanel() {
        const panel = document.createElement('div');
        panel.id = 'torn-categories-panel';
        panel.innerHTML = `
            <div class="categories-header">
                <h3>Inventory Categories</h3>
                <div class="categories-controls">
                    <button id="add-category-btn" class="torn-btn">+ Add Category</button>
                    <button id="toggle-categories-btn" class="torn-btn">Toggle</button>
                </div>
            </div>
            <div id="categories-container" class="categories-container">
                <!-- Categories will be rendered here -->
            </div>
        `;
        
        // Add styles
        addStyles();
        
        // Add event listeners
        panel.querySelector('#add-category-btn').addEventListener('click', showAddCategoryDialog);
        panel.querySelector('#toggle-categories-btn').addEventListener('click', toggleCategoriesPanel);
        
        return panel;
    }

    // Add CSS styles
    function addStyles() {
        const styles = `
            #torn-categories-panel {
                background: #2e2e2e;
                border: 1px solid #444;
                border-radius: 5px;
                margin: 10px 0;
                padding: 15px;
                color: #ddd;
            }
            
            .categories-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 15px;
                border-bottom: 1px solid #444;
                padding-bottom: 10px;
            }
            
            .categories-header h3 {
                margin: 0;
                color: #fff;
            }
            
            .categories-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 {
                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-children {
                margin-left: 20px;
                margin-top: 10px;
            }
            
            .category-drop-zone {
                min-height: 40px;
                border: 2px dashed #666;
                border-radius: 3px;
                display: flex;
                align-items: center;
                justify-content: center;
                color: #999;
                margin: 5px 0;
                transition: all 0.3s ease;
            }
            
            .category-drop-zone.drag-over {
                border-color: #4CAF50;
                background: rgba(76, 175, 80, 0.1);
                color: #4CAF50;
            }
            
            .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;
            }
            
            .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;
            }
            
            /* Draggable item styles */
            .inventory-item-draggable {
                cursor: grab;
                transition: opacity 0.3s ease;
            }
            
            .inventory-item-draggable:hover {
                opacity: 0.8;
            }
            
            .inventory-item-draggable.dragging {
                opacity: 0.5;
                cursor: grabbing;
            }
            
            /* Modal styles */
            .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);
    }

    // Make inventory items draggable
    function makeItemsDraggable() {
        // Find all inventory item elements
        const itemSelectors = [
            '.item',
            '[class*="item-"]',
            '.inventory-item',
            '.items-wrap .item'
        ];
        
        itemSelectors.forEach(selector => {
            const items = document.querySelectorAll(selector);
            items.forEach(item => {
                if (!item.classList.contains('inventory-item-draggable')) {
                    setupDraggableItem(item);
                }
            });
        });
    }

    // Setup draggable functionality for an item
    function setupDraggableItem(item) {
        item.classList.add('inventory-item-draggable');
        item.draggable = true;
        
        item.addEventListener('dragstart', (e) => {
            draggedItem = {
                element: item,
                id: getItemId(item),
                name: getItemName(item),
                image: getItemImage(item)
            };
            item.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        });
        
        item.addEventListener('dragend', (e) => {
            item.classList.remove('dragging');
            draggedItem = null;
        });
    }

    // Get item ID from element
    function getItemId(item) {
        // Try to extract item ID from various attributes
        const id = item.getAttribute('data-item') || 
                   item.getAttribute('data-id') ||
                   item.querySelector('[data-item]')?.getAttribute('data-item') ||
                   item.id ||
                   'item_' + Math.random().toString(36).substr(2, 9);
        return id;
    }

    // Get item name from element
    function getItemName(item) {
        const nameSelectors = [
            '.name',
            '.item-name',
            '.title',
            'img[alt]',
            '.desc'
        ];
        
        for (const selector of nameSelectors) {
            const element = item.querySelector(selector);
            if (element) {
                if (element.tagName === 'IMG') {
                    return element.alt || 'Unknown Item';
                }
                return element.textContent.trim() || 'Unknown Item';
            }
        }
        
        return item.textContent.trim().substring(0, 50) || 'Unknown Item';
    }

    // Get item image from element
    function getItemImage(item) {
        const img = item.querySelector('img');
        return img ? img.src : null;
    }

    // Render categories
    function renderCategories() {
        const container = document.getElementById('categories-container');
        if (!container) return;
        
        container.innerHTML = '';
        
        // Render root level categories
        const rootCategories = Object.values(categories).filter(cat => !cat.parent);
        rootCategories.forEach(category => {
            container.appendChild(renderCategory(category));
        });
    }

    // Render a single category
    function renderCategory(category) {
        const categoryDiv = document.createElement('div');
        categoryDiv.className = `category-item ${category.collapsed ? 'collapsed' : ''}`;
        categoryDiv.setAttribute('data-category-id', category.id);
        
        // Category header
        const header = document.createElement('div');
        header.className = 'category-header';
        header.innerHTML = `
            <span class="category-name">${category.name}</span>
            <div class="category-controls">
                <button onclick="addSubCategory('${category.id}')">+</button>
                <button onclick="editCategory('${category.id}')">✎</button>
                <button onclick="deleteCategory('${category.id}')">×</button>
            </div>
        `;
        
        // Toggle collapse on header click
        header.addEventListener('click', (e) => {
            if (!e.target.matches('button')) {
                toggleCategory(category.id);
            }
        });
        
        categoryDiv.appendChild(header);
        
        // Drop zone
        const dropZone = document.createElement('div');
        dropZone.className = 'category-drop-zone';
        dropZone.textContent = 'Drop items here';
        setupDropZone(dropZone, category.id);
        categoryDiv.appendChild(dropZone);
        
        // Category items
        const itemsDiv = document.createElement('div');
        itemsDiv.className = 'category-items';
        
        const categoryItems = Object.entries(itemsMapping).filter(([itemId, catId]) => catId === category.id);
        categoryItems.forEach(([itemId, catId]) => {
            const itemPreview = document.createElement('div');
            itemPreview.className = 'category-item-preview';
            itemPreview.innerHTML = `
                <span>${itemId}</span>
                <button class="remove-item-btn" onclick="removeItemFromCategory('${itemId}')">×</button>
            `;
            itemsDiv.appendChild(itemPreview);
        });
        
        categoryDiv.appendChild(itemsDiv);
        
        // Children categories
        if (category.children && category.children.length > 0) {
            const childrenDiv = document.createElement('div');
            childrenDiv.className = 'category-children';
            
            category.children.forEach(childId => {
                if (categories[childId]) {
                    childrenDiv.appendChild(renderCategory(categories[childId]));
                }
            });
            
            categoryDiv.appendChild(childrenDiv);
        }
        
        return categoryDiv;
    }

    // Setup drop zone functionality
    function setupDropZone(dropZone, categoryId) {
        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.classList.add('drag-over');
        });
        
        dropZone.addEventListener('dragleave', (e) => {
            dropZone.classList.remove('drag-over');
        });
        
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.classList.remove('drag-over');
            
            if (draggedItem) {
                addItemToCategory(draggedItem.id, draggedItem.name, categoryId);
            }
        });
    }

    // Add item to category
    function addItemToCategory(itemId, itemName, categoryId) {
        itemsMapping[itemId] = categoryId;
        saveData();
        renderCategories();
        console.log(`[Torn Categories] Added item ${itemName} to category ${categories[categoryId].name}`);
    }

    // Remove item from category
    window.removeItemFromCategory = function(itemId) {
        delete itemsMapping[itemId];
        saveData();
        renderCategories();
    };

    // Show add category dialog
    function showAddCategoryDialog(parentId = null) {
        const modal = document.createElement('div');
        modal.className = 'category-modal';
        modal.innerHTML = `
            <div class="category-modal-content">
                <h3>${parentId ? 'Add Subcategory' : 'Add Category'}</h3>
                <input type="text" id="category-name-input" placeholder="Category name" maxlength="50">
                ${parentId ? '' : `
                <select id="parent-category-select">
                    <option value="">No parent (root level)</option>
                    ${Object.values(categories).map(cat => 
                        `<option value="${cat.id}">${cat.name}</option>`
                    ).join('')}
                </select>
                `}
                <div class="category-modal-buttons">
                    <button class="torn-btn" onclick="this.closest('.category-modal').remove()">Cancel</button>
                    <button class="torn-btn" onclick="createCategory('${parentId || ''}')">Create</button>
                </div>
            </div>
        `;
        
        document.body.appendChild(modal);
        document.getElementById('category-name-input').focus();
        
        // Handle Enter key
        document.getElementById('category-name-input').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                createCategory(parentId || '');
            }
        });
    }

    // Create new category
    window.createCategory = function(parentId) {
        const nameInput = document.getElementById('category-name-input');
        const parentSelect = document.getElementById('parent-category-select');
        
        const name = nameInput.value.trim();
        if (!name) {
            alert('Please enter a category name');
            return;
        }
        
        const actualParentId = parentId || (parentSelect ? parentSelect.value : null);
        const categoryId = 'cat_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
        
        const newCategory = {
            id: categoryId,
            name: name,
            parent: actualParentId || null,
            children: [],
            collapsed: false
        };
        
        categories[categoryId] = newCategory;
        
        // Add to parent's children if parent exists
        if (actualParentId && categories[actualParentId]) {
            categories[actualParentId].children.push(categoryId);
        }
        
        saveData();
        renderCategories();
        document.querySelector('.category-modal').remove();
    };

    // Add subcategory
    window.addSubCategory = function(parentId) {
        showAddCategoryDialog(parentId);
    };

    // Edit category
    window.editCategory = function(categoryId) {
        const category = categories[categoryId];
        if (!category) return;
        
        const newName = prompt('Enter new category name:', category.name);
        if (newName && newName.trim()) {
            category.name = newName.trim();
            saveData();
            renderCategories();
        }
    };

    // Delete category
    window.deleteCategory = function(categoryId) {
        if (categoryId === 'default') {
            alert('Cannot delete the default category');
            return;
        }
        
        const category = categories[categoryId];
        if (!category) return;
        
        if (!confirm(`Delete category "${category.name}" and all its subcategories?`)) {
            return;
        }
        
        // Move items to default category
        Object.keys(itemsMapping).forEach(itemId => {
            if (itemsMapping[itemId] === categoryId) {
                itemsMapping[itemId] = 'default';
            }
        });
        
        // Delete recursively
        deleteCategoryRecursive(categoryId);
        
        saveData();
        renderCategories();
    };

    // Delete category and all children recursively
    function deleteCategoryRecursive(categoryId) {
        const category = categories[categoryId];
        if (!category) return;
        
        // Delete children first
        if (category.children) {
            category.children.forEach(childId => {
                deleteCategoryRecursive(childId);
            });
        }
        
        // Remove from parent's children
        if (category.parent && categories[category.parent]) {
            const parentChildren = categories[category.parent].children;
            const index = parentChildren.indexOf(categoryId);
            if (index > -1) {
                parentChildren.splice(index, 1);
            }
        }
        
        // Delete the category
        delete categories[categoryId];
    }

    // Toggle category collapse
    function toggleCategory(categoryId) {
        if (categories[categoryId]) {
            categories[categoryId].collapsed = !categories[categoryId].collapsed;
            saveData();
            renderCategories();
        }
    }

    // Toggle categories panel
    function toggleCategoriesPanel() {
        const container = document.getElementById('categories-container');
        if (container) {
            container.style.display = container.style.display === 'none' ? 'block' : 'none';
        }
    }

    // Setup observer for dynamic content
    function setupInventoryObserver() {
        const observer = new MutationObserver((mutations) => {
            let shouldUpdate = false;
            
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1) { // Element node
                        // Check if new inventory items were added
                        if (node.matches && (node.matches('.item') || node.querySelector('.item'))) {
                            shouldUpdate = true;
                        }
                    }
                });
            });
            
            if (shouldUpdate) {
                setTimeout(() => {
                    makeItemsDraggable();
                }, 100);
            }
        });
        
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
    
    // Also try to initialize after a short delay for dynamic content
    setTimeout(init, 1000);

})();