Torn Inventory Categories

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

目前為 2025-07-27 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Inventory Categories
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @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" id="cancel-category-btn">Cancel</button>
                    <button class="torn-btn" id="create-category-btn">Create</button>
                </div>
            </div>
        `;
        
        document.body.appendChild(modal);
        document.getElementById('category-name-input').focus();
        
        // Add event listeners for buttons
        document.getElementById('cancel-category-btn').addEventListener('click', () => {
            modal.remove();
        });
        
        document.getElementById('create-category-btn').addEventListener('click', () => {
            createCategory(parentId || '');
        });
        
        // Handle Enter key
        document.getElementById('category-name-input').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                createCategory(parentId || '');
            }
        });
        
        // Handle Escape key
        document.addEventListener('keydown', function escapeHandler(e) {
            if (e.key === 'Escape') {
                modal.remove();
                document.removeEventListener('keydown', escapeHandler);
            }
        });
        
        // Handle click outside modal
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                modal.remove();
            }
        });
    }

    // Create new category
    function createCategory(parentId) {
        const nameInput = document.getElementById('category-name-input');
        const parentSelect = document.getElementById('parent-category-select');
        
        const name = nameInput ? 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();
        
        // Remove the modal
        const modal = document.querySelector('.category-modal');
        if (modal) {
            modal.remove();
        }
        
        console.log(`[Torn Categories] Created category: ${name} (ID: ${categoryId})`);
    }
    
    // Make createCategory available globally for backward compatibility
    window.createCategory = createCategory;

    // 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);

})();