您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Create custom categories for your Torn inventory with drag and drop functionality
当前为
// ==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); })();