您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Manage your Torn inventory with custom categories
当前为
// ==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* // @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; // Check if we're on the correct page if (!isInventoryPage()) { console.log('[Torn Inventory] Not on inventory page, skipping initialization'); return; } console.log('[Torn Inventory] Initializing on inventory page...'); loadData(); createCategoryInterface(); // Add a small delay before trying to find items setTimeout(() => { console.log('[Torn Inventory] Looking for inventory items...'); setupInventoryObserver(); addInventoryControlsToItems(); }, 1000); 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); } // 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); } } // Create the category interface function createCategoryInterface() { // Find inventory container const inventoryContainer = findInventoryContainer(); if (!inventoryContainer) { console.warn('[Torn Inventory] Inventory container not found'); return; } // Create categories panel const categoriesPanel = createCategoriesPanel(); // Insert categories panel before inventory inventoryContainer.parentNode.insertBefore(categoriesPanel, inventoryContainer); // Render categories renderCategories(); } // Find the inventory container function findInventoryContainer() { console.log('[Torn Inventory] Searching for inventory container...'); // Try multiple selectors for different inventory layouts const selectors = [ '.items-wrap', '.inventory-wrap', '#inventory', '.item-list', '.items-cont', '.item-list-wrap', '.your-items', '[class*="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; } } // Fallback: look for ul.all-items specifically 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"> <!-- Categories will be rendered here --> </div> `; // Add styles addStyles(); // Add event listeners 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; } /* Item category controls */ .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; } /* Quick categorize menu */ .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; } /* Green flasher animation for highlighting items */ @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; } /* 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 have category controls function addInventoryControlsToItems() { console.log('[Torn Inventory] Starting to add category controls to items...'); const inventoryItems = findInventoryItems(); console.log(`[Torn Inventory] Found ${inventoryItems.length} inventory items to process`); inventoryItems.forEach((item, index) => { console.log(`[Torn Inventory] Processing item ${index + 1}:`, item); if (!item.querySelector('.torn-inventory-control')) { addInventoryControlToItem(item); } else { console.log('[Torn Inventory] Item already has category control, skipping'); } }); } // Find actual inventory items function findInventoryItems() { const items = []; const inventoryContainer = findInventoryContainer(); if (!inventoryContainer) { console.warn('[Torn Inventory] No inventory container found'); return items; } console.log('[Torn Inventory] Looking for items in container:', inventoryContainer); const itemSelectors = [ 'ul.all-items li[data-item]', 'ul.items-cont li[data-item]', 'ul.itemsList li[data-item]', 'li[data-item]', 'li[data-equipped]', '.all-items li', '.items-cont li' ]; itemSelectors.forEach(selector => { const elements = inventoryContainer.querySelectorAll(selector); console.log(`[Torn Inventory] Found ${elements.length} items with selector: ${selector}`); elements.forEach(element => { if (isValidInventoryItem(element) && !items.includes(element)) { items.push(element); console.log('[Torn Inventory] Added valid inventory item:', element.getAttribute('data-item'), getItemName(element)); } }); }); console.log(`[Torn Inventory] Total inventory items found: ${items.length}`); return items; } // Validate if an element is actually an inventory item function isValidInventoryItem(element) { if (element.querySelector('.torn-inventory-control')) { return false; } const isCorrectElement = element.tagName === 'LI' && element.hasAttribute('data-item'); if (!isCorrectElement) { return false; } const hasItemContent = element.querySelector('.name, .title-wrap, img[alt]'); const isVisible = element.offsetWidth > 0 && element.offsetHeight > 0; const inInventoryArea = element.closest('.all-items, .items-cont, .itemsList'); const isValid = hasItemContent && isVisible && inInventoryArea; if (isValid) { console.log('[Torn Inventory] Valid inventory item found:', { element: element, dataItem: element.getAttribute('data-item'), itemName: getItemName(element), tagName: element.tagName }); } return isValid; } // Add category control to a single item function addInventoryControlToItem(item) { const itemId = getItemId(item); const itemName = getItemName(item); // Create control container const controlContainer = document.createElement('div'); controlContainer.className = 'torn-inventory-control'; controlContainer.innerHTML = ` <select class="category-selector" data-item-id="${itemId}" data-item-name="${itemName}"> <option value="">Select Category...</option> ${generateCategoryOptions()} </select> <button class="category-quick-btn" data-item-id="${itemId}" data-item-name="${itemName}" title="Quick categorize">📁</button> `; // Set current category if item is already categorized const currentCategory = itemsMapping[itemId]; if (currentCategory) { const selector = controlContainer.querySelector('.category-selector'); selector.value = currentCategory; } // Add event listeners const selector = controlContainer.querySelector('.category-selector'); selector.addEventListener('change', (e) => { const newCategoryId = e.target.value; const itemId = e.target.getAttribute('data-item-id'); const itemName = e.target.getAttribute('data-item-name'); if (newCategoryId) { addItemToCategory(itemId, itemName, newCategoryId); } else { removeItemFromCategory(itemId); } }); const quickBtn = controlContainer.querySelector('.category-quick-btn'); quickBtn.addEventListener('click', (e) => { const itemId = e.target.getAttribute('data-item-id'); const itemName = e.target.getAttribute('data-item-name'); showQuickCategorizeMenu(e.target, itemId, itemName); }); insertInventoryControl(item, controlContainer); } // Generate category options HTML function generateCategoryOptions() { let options = ''; function addCategoryOptions(categoryList, indent = '') { categoryList.forEach(category => { options += `<option value="${category.id}">${indent}${category.name}</option>`; if (category.children && category.children.length > 0) { const children = category.children.map(childId => categories[childId]).filter(Boolean); addCategoryOptions(children, indent + '→ '); } }); } const rootCategories = Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ); addCategoryOptions(rootCategories); return options; } // Insert category control into item element function insertInventoryControl(item, controlContainer) { console.log('[Torn Inventory] Inserting control into item:', getItemName(item)); const insertionTargets = [ item.querySelector('.cont-wrap'), item.querySelector('.actions'), item.querySelector('.title-wrap'), item.querySelector('.outside-actions'), item ]; for (const target of insertionTargets) { if (target && target !== item) { const wrapper = document.createElement('div'); wrapper.style.cssText = 'clear: both; margin: 5px 0;'; wrapper.appendChild(controlContainer); target.parentNode.insertBefore(wrapper, target.nextSibling); console.log('[Torn Inventory] Added control after:', target.className || target.tagName); return; } } if (item) { const wrapper = document.createElement('div'); wrapper.style.cssText = 'clear: both; margin: 5px 0; padding: 5px; background: rgba(0,0,0,0.1); border-radius: 3px;'; wrapper.appendChild(controlContainer); item.appendChild(wrapper); console.log('[Torn Inventory] Added control to item directly'); } else { console.warn('[Torn Inventory] Could not find suitable location to insert control'); } } // Show quick categorize menu function showQuickCategorizeMenu(button, itemId, itemName) { document.querySelectorAll('.torn-quick-category-menu').forEach(menu => menu.remove()); const menu = document.createElement('div'); menu.className = 'torn-quick-category-menu'; const rootCategories = Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ); let menuHTML = '<div class="quick-menu-title">Quick Categorize</div>'; rootCategories.slice(0, 5).forEach(category => { menuHTML += `<button class="quick-category-btn" data-category-id="${category.id}" data-item-id="${itemId}" data-item-name="${itemName}">${category.name}</button>`; }); menuHTML += `<button class="quick-category-btn remove-btn" data-item-id="${itemId}">Remove from category</button>`; menuHTML += `<button class="quick-category-btn close-btn">Close</button>`; menu.innerHTML = menuHTML; const rect = button.getBoundingClientRect(); menu.style.position = 'fixed'; menu.style.top = (rect.bottom + 5) + 'px'; menu.style.left = rect.left + 'px'; menu.style.zIndex = '10001'; menu.addEventListener('click', (e) => { const categoryId = e.target.getAttribute('data-category-id'); const itemId = e.target.getAttribute('data-item-id'); const itemName = e.target.getAttribute('data-item-name'); if (e.target.classList.contains('remove-btn')) { removeItemFromCategory(itemId); menu.remove(); } else if (e.target.classList.contains('close-btn')) { menu.remove(); } else if (categoryId) { addItemToCategory(itemId, itemName, categoryId); menu.remove(); } }); document.body.appendChild(menu); setTimeout(() => { document.addEventListener('click', function closeMenu(e) { if (!menu.contains(e.target) && e.target !== button) { menu.remove(); document.removeEventListener('click', closeMenu); } }); }, 100); } // Get item ID from element function getItemId(item) { 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'; } // Highlight an item in the actual inventory function highlightInventoryItem(itemId) { console.log('[Torn Inventory] Attempting to highlight item:', itemId); // Find the item in the inventory const inventoryContainer = document.querySelector('#all-items, ul.all-items, .all-items'); if (!inventoryContainer) { console.warn('[Torn Inventory] Could not find inventory container for highlighting'); return; } // Look for the item by data-item attribute const inventoryItem = inventoryContainer.querySelector(`[data-item="${itemId}"]`); if (!inventoryItem) { console.warn('[Torn Inventory] Could not find item in inventory:', itemId); return; } // Remove any existing highlights document.querySelectorAll('.inventory-item-highlighted').forEach(item => { item.classList.remove('inventory-item-highlighted'); }); // Add highlight class inventoryItem.classList.add('inventory-item-highlighted'); // Scroll to the item inventoryItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Remove highlight after animation setTimeout(() => { inventoryItem.classList.remove('inventory-item-highlighted'); }, 3000); console.log('[Torn Inventory] Successfully highlighted item:', itemId); } // Move category up in order function moveCategoryUp(categoryId) { const category = categories[categoryId]; if (!category) return; // Get sibling categories const siblingCategories = category.parent ? categories[category.parent]?.children || [] : Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ).map(cat => cat.id); const currentIndex = siblingCategories.indexOf(categoryId); if (currentIndex <= 0) return; // Already at top // Swap order values const currentCategory = categories[categoryId]; const previousCategory = categories[siblingCategories[currentIndex - 1]]; const tempOrder = currentCategory.order || currentIndex; currentCategory.order = previousCategory.order || (currentIndex - 1); previousCategory.order = tempOrder; saveData(); renderCategories(); updateItemCategoryControls(); console.log('[Torn Inventory] Moved category up:', category.name); } // Move category down in order function moveCategoryDown(categoryId) { const category = categories[categoryId]; if (!category) return; // Get sibling categories const siblingCategories = category.parent ? categories[category.parent]?.children || [] : Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ).map(cat => cat.id); const currentIndex = siblingCategories.indexOf(categoryId); if (currentIndex >= siblingCategories.length - 1) return; // Already at bottom // Swap order values const currentCategory = categories[categoryId]; const nextCategory = categories[siblingCategories[currentIndex + 1]]; const tempOrder = currentCategory.order || currentIndex; currentCategory.order = nextCategory.order || (currentIndex + 1); nextCategory.order = tempOrder; saveData(); renderCategories(); updateItemCategoryControls(); console.log('[Torn Inventory] Moved category down:', category.name); } // Add item to category function addItemToCategory(itemId, itemName, categoryId) { itemsMapping[itemId] = categoryId; saveData(); renderCategories(); updateItemCategoryControls(); console.log(`[Torn Inventory] Added item ${itemName} to category ${categories[categoryId].name}`); } // Remove item from category function removeItemFromCategory(itemId) { delete itemsMapping[itemId]; saveData(); renderCategories(); updateItemCategoryControls(); console.log(`[Torn Inventory] Removed item from category`); } // Update all item category controls function updateItemCategoryControls() { console.log('[Torn Inventory] Updating item category controls...'); addInventoryControlsToItems(); // Update all existing selectors with new category options document.querySelectorAll('.category-selector').forEach(selector => { const itemId = selector.getAttribute('data-item-id'); const currentCategory = itemsMapping[itemId]; // Update the options selector.innerHTML = ` <option value="">Select Category...</option> ${generateCategoryOptions()} `; // Restore the selected value selector.value = currentCategory || ''; console.log('[Torn Inventory] Updated control for item:', itemId, 'category:', currentCategory); }); } // Render categories function renderCategories() { const container = document.getElementById('categories-container'); if (!container) { console.error('[Torn Inventory] Categories container not found'); return; } container.innerHTML = ''; console.log('[Torn Inventory] Rendering categories:', categories); const rootCategories = Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ).sort((a, b) => (a.order || 0) - (b.order || 0)); console.log('[Torn Inventory] Root categories found:', rootCategories.length); if (rootCategories.length === 0) { container.innerHTML = '<div style="color: #999; padding: 20px; text-align: center;">No categories found. Click "Add Category" to create one.</div>'; return; } rootCategories.forEach(category => { console.log('[Torn Inventory] Rendering category:', category.name); container.appendChild(renderCategory(category)); }); } // Render a single category function renderCategory(category) { console.log('[Torn Inventory] Rendering single category:', category); const categoryDiv = document.createElement('div'); categoryDiv.className = `category-item ${category.collapsed ? 'collapsed' : ''}`; categoryDiv.setAttribute('data-category-id', category.id); const header = document.createElement('div'); header.className = 'category-header'; // Get sibling categories for reorder buttons const siblingCategories = category.parent ? categories[category.parent]?.children || [] : Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ).map(cat => cat.id); const currentIndex = siblingCategories.indexOf(category.id); const isFirst = currentIndex === 0; const isLast = currentIndex === siblingCategories.length - 1; header.innerHTML = ` <span class="category-name">${category.name}</span> <div class="category-controls"> <div class="category-reorder-controls"> <button class="reorder-btn" data-action="move-up" data-category-id="${category.id}" ${isFirst ? 'disabled' : ''} title="Move up">↑</button> <button class="reorder-btn" data-action="move-down" data-category-id="${category.id}" ${isLast ? 'disabled' : ''} title="Move down">↓</button> </div> <button data-action="add-sub" data-category-id="${category.id}">+</button> <button data-action="edit" data-category-id="${category.id}">✎</button> <button data-action="delete" data-category-id="${category.id}">×</button> </div> `; header.addEventListener('click', (e) => { const action = e.target.getAttribute('data-action'); const categoryId = e.target.getAttribute('data-category-id'); if (action === 'add-sub') { e.stopPropagation(); addSubCategory(categoryId); } else if (action === 'edit') { e.stopPropagation(); editCategory(categoryId); } else if (action === 'delete') { e.stopPropagation(); deleteCategory(categoryId); } else if (action === 'move-up') { e.stopPropagation(); moveCategoryUp(categoryId); } else if (action === 'move-down') { e.stopPropagation(); moveCategoryDown(categoryId); } else if (!e.target.matches('button')) { toggleCategory(category.id); } }); categoryDiv.appendChild(header); // Category items display 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" data-item-id="${itemId}">×</button> `; // Add click event to highlight item in inventory itemPreview.addEventListener('click', (e) => { if (!e.target.classList.contains('remove-item-btn')) { highlightInventoryItem(itemId); } }); itemPreview.querySelector('.remove-item-btn').addEventListener('click', (e) => { e.stopPropagation(); // Prevent highlighting when removing const itemId = e.target.getAttribute('data-item-id'); removeItemFromCategory(itemId); }); itemsDiv.appendChild(itemPreview); }); categoryDiv.appendChild(itemsDiv); // Children categories if (category.children && category.children.length > 0) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'category-children'; // Sort children by order const sortedChildren = category.children .map(childId => categories[childId]) .filter(Boolean) .sort((a, b) => (a.order || 0) - (b.order || 0)); sortedChildren.forEach(childCategory => { childrenDiv.appendChild(renderCategory(childCategory)); }); categoryDiv.appendChild(childrenDiv); } return categoryDiv; } // Show add category dialog function showAddCategoryDialog(parentId) { console.log('[Torn Inventory] showAddCategoryDialog called with parentId:', parentId); const modal = document.createElement('div'); modal.className = 'category-modal'; const isSubcategory = parentId && parentId !== '' && parentId !== 'undefined'; console.log('[Torn Inventory] Is subcategory:', isSubcategory, 'parentId:', parentId); modal.innerHTML = ` <div class="category-modal-content"> <h3>${isSubcategory ? 'Add Subcategory' : 'Add Category'}</h3> <input type="text" id="category-name-input" placeholder="Category name" maxlength="50"> ${isSubcategory ? '' : ` <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(isSubcategory ? parentId : null); }); // Handle Enter key document.getElementById('category-name-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { createCategory(isSubcategory ? parentId : null); } }); // 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) { console.log('[Torn Inventory] createCategory called with parentId:', 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; } // Determine the actual parent ID let actualParentId = null; if (parentId && parentId !== '' && parentId !== 'null' && parentId !== 'undefined') { actualParentId = parentId; console.log('[Torn Inventory] Creating subcategory with parent:', actualParentId); } else if (parentSelect && parentSelect.value && parentSelect.value !== '') { actualParentId = parentSelect.value; console.log('[Torn Inventory] Creating category with selected parent:', actualParentId); } else { actualParentId = null; console.log('[Torn Inventory] Creating root level category'); } const categoryId = 'cat_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); const newCategory = { id: categoryId, name: name, parent: actualParentId, children: [], collapsed: false, order: getNextOrderValue(actualParentId) }; categories[categoryId] = newCategory; // Add to parent's children if parent exists if (actualParentId && categories[actualParentId]) { if (!categories[actualParentId].children) { categories[actualParentId].children = []; } categories[actualParentId].children.push(categoryId); } saveData(); renderCategories(); updateItemCategoryControls(); // Remove the modal const modal = document.querySelector('.category-modal'); if (modal) { modal.remove(); } console.log(`[Torn Inventory] Created category: ${name} (ID: ${categoryId}), Parent: ${actualParentId || 'none (root level)'}`); } // Get next order value for a parent category function getNextOrderValue(parentId) { const siblingCategories = parentId ? categories[parentId]?.children.map(id => categories[id]).filter(Boolean) || [] : Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ); if (siblingCategories.length === 0) return 0; const maxOrder = Math.max(...siblingCategories.map(cat => cat.order || 0)); return maxOrder + 1; } // Add subcategory function addSubCategory(parentId) { showAddCategoryDialog(parentId); } // Edit category function editCategory(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(); updateItemCategoryControls(); } } // Delete category function deleteCategory(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(); updateItemCategoryControls(); } // 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(); } } // Reset all categories function resetAllCategories() { if (!confirm('Are you sure you want to delete ALL categories and item assignments? This cannot be undone!')) { return; } // Reset to default state categories = { 'default': { id: 'default', name: 'Uncategorized', parent: null, children: [], collapsed: false, order: 0 } }; itemsMapping = {}; saveData(); renderCategories(); updateItemCategoryControls(); console.log('[Torn Inventory] All categories and assignments have been reset'); alert('All categories have been reset! You now have a clean "Uncategorized" category to start with.'); } // 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 inventory page 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(() => { addInventoryControlsToItems(); }, 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); })();