您需要先安装一个扩展,例如 篡改猴、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.2 // @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()) { console.log('[Torn Categories] Not on inventory page, skipping initialization'); return; } console.log('[Torn Categories] Initializing on inventory page...'); loadData(); createCategoryInterface(); // Add a small delay before trying to find items setTimeout(() => { console.log('[Torn Categories] Looking for inventory items...'); setupInventoryObserver(); addCategoryControlsToItems(); }, 1000); 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, .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); // 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 have category controls addCategoryControlsToItems(); // Render categories renderCategories(); } // Find the inventory container function findInventoryContainer() { console.log('[Torn Categories] Searching for inventory container...'); // Try multiple selectors for different inventory layouts const selectors = [ '.items-wrap', '.inventory-wrap', '#inventory', '.item-list', '.items-cont', // For list view like in your screenshot '.item-list-wrap', '.your-items', '[class*="items"]' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { console.log('[Torn Categories] Found inventory container with selector:', selector); console.log('[Torn Categories] Container element:', element); return element; } } // Fallback: look for ul.all-items specifically const allItems = document.querySelector('ul.all-items'); if (allItems) { console.log('[Torn Categories] Found ul.all-items container'); return allItems.parentElement || allItems; } console.warn('[Torn Categories] No inventory container found'); console.log('[Torn Categories] Available elements:', { itemsWrap: document.querySelector('.items-wrap'), allItems: document.querySelector('ul.all-items'), itemsList: document.querySelector('.itemsList'), itemsCont: document.querySelector('.items-cont') }); 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="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(); // No parameters for root category }); 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-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: 30px; border: 1px solid #555; border-radius: 3px; display: flex; align-items: center; justify-content: center; color: #999; margin: 5px 0; background: #333; } /* Item category controls */ .torn-category-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; } .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 have category controls function addCategoryControlsToItems() { console.log('[Torn Categories] Starting to add category controls to items...'); // Only target actual inventory items, not other elements with "item" in class name const inventoryItems = findInventoryItems(); console.log(`[Torn Categories] Found ${inventoryItems.length} inventory items to process`); inventoryItems.forEach((item, index) => { console.log(`[Torn Categories] Processing item ${index + 1}:`, item); if (!item.querySelector('.torn-category-control')) { addCategoryControlToItem(item); } else { console.log('[Torn Categories] Item already has category control, skipping'); } }); } // Find actual inventory items (not other page elements) function findInventoryItems() { const items = []; // Look for the main inventory container first const inventoryContainer = findInventoryContainer(); if (!inventoryContainer) { console.warn('[Torn Categories] No inventory container found'); return items; } console.log('[Torn Categories] Looking for items in container:', inventoryContainer); // Target the specific inventory list structure from Torn const itemSelectors = [ // Torn's specific inventory structure 'ul.all-items li[data-item]', 'ul.items-cont li[data-item]', 'ul.itemsList li[data-item]', // Broader selectors as fallback 'li[data-item]', 'li[data-equipped]', '.all-items li', '.items-cont li' ]; itemSelectors.forEach(selector => { const elements = inventoryContainer.querySelectorAll(selector); console.log(`[Torn Categories] Found ${elements.length} items with selector: ${selector}`); elements.forEach(element => { // Additional validation to ensure this is actually an inventory item if (isValidInventoryItem(element) && !items.includes(element)) { items.push(element); console.log('[Torn Categories] Added valid inventory item:', element.getAttribute('data-item'), getItemName(element)); } }); }); console.log(`[Torn Categories] Total inventory items found: ${items.length}`); return items; } // Validate if an element is actually an inventory item function isValidInventoryItem(element) { // Skip if already processed if (element.querySelector('.torn-category-control')) { return false; } // Must be an LI element with data-item attribute (Torn's structure) const isCorrectElement = element.tagName === 'LI' && element.hasAttribute('data-item'); if (!isCorrectElement) { return false; } // Must have item content (images, name, etc.) const hasItemContent = element.querySelector('.name, .title-wrap, img[alt]'); // Must be visible const isVisible = element.offsetWidth > 0 && element.offsetHeight > 0; // Must be in the inventory area const inInventoryArea = element.closest('.all-items, .items-cont, .itemsList'); const isValid = hasItemContent && isVisible && inInventoryArea; if (isValid) { console.log('[Torn Categories] 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 addCategoryControlToItem(item) { const itemId = getItemId(item); const itemName = getItemName(item); // Create control container const controlContainer = document.createElement('div'); controlContainer.className = 'torn-category-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); }); // Insert the control into the item insertCategoryControl(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>`; // Add children with indentation if (category.children && category.children.length > 0) { const children = category.children.map(childId => categories[childId]).filter(Boolean); addCategoryOptions(children, indent + '→ '); } }); } // Start with root categories 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 insertCategoryControl(item, controlContainer) { console.log('[Torn Categories] Inserting control into item:', getItemName(item)); // For Torn's LI structure, find the best insertion point const insertionTargets = [ item.querySelector('.cont-wrap'), // Main content wrapper item.querySelector('.actions'), // Actions area item.querySelector('.title-wrap'), // Title wrapper item.querySelector('.outside-actions'), // Outside actions item // Item itself as fallback ]; for (const target of insertionTargets) { if (target && target !== item) { // Insert after the target element 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 Categories] Added control after:', target.className || target.tagName); return; } } // Fallback: append to the item directly 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 Categories] Added control to item directly'); } else { console.warn('[Torn Categories] Could not find suitable location to insert control'); } } // Show quick categorize menu function showQuickCategorizeMenu(button, itemId, itemName) { // Remove any existing quick menus document.querySelectorAll('.torn-quick-category-menu').forEach(menu => menu.remove()); const menu = document.createElement('div'); menu.className = 'torn-quick-category-menu'; // Get root categories for quick access 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 => { // Show first 5 categories 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; // Position menu near button 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'; // Add event listeners 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); // Close menu when clicking outside 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) { // 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) { console.error('[Torn Categories] Categories container not found'); return; } container.innerHTML = ''; // Debug logging console.log('[Torn Categories] Rendering categories:', categories); // Render root level categories (parent is null, undefined, or empty string) const rootCategories = Object.values(categories).filter(cat => cat.parent === null || cat.parent === undefined || cat.parent === '' ); console.log('[Torn Categories] Root categories found:', rootCategories.length); console.log('[Torn Categories] Root categories:', rootCategories.map(cat => `${cat.name} (parent: ${cat.parent})`)); 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 Categories] Rendering category:', category.name); container.appendChild(renderCategory(category)); }); } // Render a single category function renderCategory(category) { console.log('[Torn Categories] Rendering single category:', 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 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> `; // Add event listeners for category controls 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 (!e.target.matches('button')) { // Toggle collapse on header click (but not on buttons) toggleCategory(category.id); } }); categoryDiv.appendChild(header); // Drop zone (now just for visual purposes) const dropZone = document.createElement('div'); dropZone.className = 'category-drop-zone'; dropZone.innerHTML = '<span style="font-size: 12px; color: #999;">Items in this category will appear here</span>'; 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" data-item-id="${itemId}">×</button> `; // Add event listener for remove button itemPreview.querySelector('.remove-item-btn').addEventListener('click', (e) => { 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'; 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(); updateItemCategoryControls(); // Update all item controls to reflect changes console.log(`[Torn Categories] Added item ${itemName} to category ${categories[categoryId].name}`); } // Remove item from category function removeItemFromCategory(itemId) { delete itemsMapping[itemId]; saveData(); renderCategories(); updateItemCategoryControls(); // Update all item controls to reflect changes console.log(`[Torn Categories] Removed item from category`); } // Update all item category controls to reflect current state function updateItemCategoryControls() { console.log('[Torn Categories] Updating item category controls...'); // First make sure controls exist addCategoryControlsToItems(); // Then update their values document.querySelectorAll('.category-selector').forEach(selector => { const itemId = selector.getAttribute('data-item-id'); const currentCategory = itemsMapping[itemId]; selector.value = currentCategory || ''; console.log('[Torn Categories] Updated control for item:', itemId, 'category:', currentCategory); }); } // Remove item from category window.removeItemFromCategory = function(itemId) { delete itemsMapping[itemId]; saveData(); renderCategories(); }; // Show add category dialog function showAddCategoryDialog(parentId) { console.log('[Torn Categories] showAddCategoryDialog called with parentId:', parentId); const modal = document.createElement('div'); modal.className = 'category-modal'; const isSubcategory = parentId && parentId !== '' && parentId !== 'undefined'; console.log('[Torn Categories] 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 Categories] 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') { // Creating subcategory with specific parent actualParentId = parentId; console.log('[Torn Categories] Creating subcategory with parent:', actualParentId); } else if (parentSelect && parentSelect.value && parentSelect.value !== '') { // Creating category with selected parent from dropdown actualParentId = parentSelect.value; console.log('[Torn Categories] Creating category with selected parent:', actualParentId); } else { // Creating root level category actualParentId = null; console.log('[Torn Categories] 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 }; 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(); // Remove the modal const modal = document.querySelector('.category-modal'); if (modal) { modal.remove(); } console.log(`[Torn Categories] Created category: ${name} (ID: ${categoryId}), Parent: ${actualParentId || 'none (root level)'}`); } // 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(); } } // 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 } }; itemsMapping = {}; saveData(); renderCategories(); updateItemCategoryControls(); console.log('[Torn Categories] 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 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(() => { addCategoryControlsToItems(); }, 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); })();