// ==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);
})();