// ==UserScript==
// @name Torn Inventory Categories
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description Create custom categories for your Torn inventory with drag and drop functionality
// @author TornUser
// @match https://www.torn.com/item.php*
// @match https://www.torn.com/index.php?page=items*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// Configuration
const STORAGE_KEY = 'torn_inventory_categories';
const ITEMS_KEY = 'torn_inventory_items_mapping';
// Global variables
let categories = {};
let itemsMapping = {};
let draggedItem = null;
let isInitialized = false;
// Initialize the script
function init() {
if (isInitialized) return;
// Check if we're on the correct page
if (!isInventoryPage()) return;
loadData();
createCategoryInterface();
setupInventoryObserver();
isInitialized = true;
console.log('[Torn Categories] Inventory Categories loaded successfully');
}
// Check if current page is inventory
function isInventoryPage() {
return window.location.href.includes('item.php') ||
window.location.href.includes('page=items') ||
document.querySelector('.items-wrap') !== null;
}
// Load saved data from storage
function loadData() {
try {
const savedCategories = GM_getValue(STORAGE_KEY, '{}');
const savedItems = GM_getValue(ITEMS_KEY, '{}');
categories = JSON.parse(savedCategories);
itemsMapping = JSON.parse(savedItems);
// Initialize with default category if empty
if (Object.keys(categories).length === 0) {
categories = {
'default': {
id: 'default',
name: 'Uncategorized',
parent: null,
children: [],
collapsed: false
}
};
saveData();
}
} catch (error) {
console.error('[Torn Categories] Error loading data:', error);
categories = {
'default': {
id: 'default',
name: 'Uncategorized',
parent: null,
children: [],
collapsed: false
}
};
itemsMapping = {};
}
}
// Save data to storage
function saveData() {
try {
GM_setValue(STORAGE_KEY, JSON.stringify(categories));
GM_setValue(ITEMS_KEY, JSON.stringify(itemsMapping));
} catch (error) {
console.error('[Torn Categories] Error saving data:', error);
}
}
// Create the category interface
function createCategoryInterface() {
// Find inventory container
const inventoryContainer = findInventoryContainer();
if (!inventoryContainer) {
console.warn('[Torn Categories] Inventory container not found');
return;
}
// Create categories panel
const categoriesPanel = createCategoriesPanel();
// Insert categories panel before inventory
inventoryContainer.parentNode.insertBefore(categoriesPanel, inventoryContainer);
// Modify inventory items to be draggable
makeItemsDraggable();
// Render categories
renderCategories();
}
// Find the inventory container
function findInventoryContainer() {
// Try multiple selectors for different inventory layouts
const selectors = [
'.items-wrap',
'.inventory-wrap',
'#inventory',
'.item-list',
'.items-cont'
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) return element;
}
// Fallback: look for elements containing item classes
const itemElements = document.querySelectorAll('[class*="item"]');
if (itemElements.length > 0) {
return itemElements[0].closest('.content-wrapper, .main-content, body');
}
return null;
}
// Create the categories panel
function createCategoriesPanel() {
const panel = document.createElement('div');
panel.id = 'torn-categories-panel';
panel.innerHTML = `
<div class="categories-header">
<h3>Inventory Categories</h3>
<div class="categories-controls">
<button id="add-category-btn" class="torn-btn">+ Add Category</button>
<button id="toggle-categories-btn" class="torn-btn">Toggle</button>
</div>
</div>
<div id="categories-container" class="categories-container">
<!-- Categories will be rendered here -->
</div>
`;
// Add styles
addStyles();
// Add event listeners
panel.querySelector('#add-category-btn').addEventListener('click', showAddCategoryDialog);
panel.querySelector('#toggle-categories-btn').addEventListener('click', toggleCategoriesPanel);
return panel;
}
// Add CSS styles
function addStyles() {
const styles = `
#torn-categories-panel {
background: #2e2e2e;
border: 1px solid #444;
border-radius: 5px;
margin: 10px 0;
padding: 15px;
color: #ddd;
}
.categories-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid #444;
padding-bottom: 10px;
}
.categories-header h3 {
margin: 0;
color: #fff;
}
.categories-controls {
display: flex;
gap: 10px;
}
.torn-btn {
background: #4a4a4a;
border: 1px solid #666;
color: #ddd;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.torn-btn:hover {
background: #555;
}
.categories-container {
max-height: 300px;
overflow-y: auto;
}
.category-item {
background: #3a3a3a;
border: 1px solid #555;
border-radius: 3px;
margin: 5px 0;
padding: 10px;
position: relative;
}
.category-item.collapsed .category-children {
display: none;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.category-name {
font-weight: bold;
color: #fff;
}
.category-controls {
display: flex;
gap: 5px;
}
.category-controls button {
background: #555;
border: none;
color: #ddd;
padding: 2px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
.category-controls button:hover {
background: #666;
}
.category-children {
margin-left: 20px;
margin-top: 10px;
}
.category-drop-zone {
min-height: 40px;
border: 2px dashed #666;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
margin: 5px 0;
transition: all 0.3s ease;
}
.category-drop-zone.drag-over {
border-color: #4CAF50;
background: rgba(76, 175, 80, 0.1);
color: #4CAF50;
}
.category-items {
margin-top: 10px;
}
.category-item-preview {
background: #4a4a4a;
border: 1px solid #666;
padding: 5px;
margin: 2px 0;
border-radius: 2px;
font-size: 11px;
display: flex;
justify-content: space-between;
align-items: center;
}
.remove-item-btn {
background: #d32f2f;
border: none;
color: white;
padding: 1px 4px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
.remove-item-btn:hover {
background: #f44336;
}
/* Draggable item styles */
.inventory-item-draggable {
cursor: grab;
transition: opacity 0.3s ease;
}
.inventory-item-draggable:hover {
opacity: 0.8;
}
.inventory-item-draggable.dragging {
opacity: 0.5;
cursor: grabbing;
}
/* Modal styles */
.category-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.category-modal-content {
background: #2e2e2e;
border: 1px solid #444;
border-radius: 5px;
padding: 20px;
max-width: 400px;
width: 90%;
color: #ddd;
}
.category-modal input, .category-modal select {
width: 100%;
padding: 8px;
margin: 10px 0;
background: #4a4a4a;
border: 1px solid #666;
border-radius: 3px;
color: #ddd;
}
.category-modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 15px;
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
// Make inventory items draggable
function makeItemsDraggable() {
// Find all inventory item elements
const itemSelectors = [
'.item',
'[class*="item-"]',
'.inventory-item',
'.items-wrap .item'
];
itemSelectors.forEach(selector => {
const items = document.querySelectorAll(selector);
items.forEach(item => {
if (!item.classList.contains('inventory-item-draggable')) {
setupDraggableItem(item);
}
});
});
}
// Setup draggable functionality for an item
function setupDraggableItem(item) {
item.classList.add('inventory-item-draggable');
item.draggable = true;
item.addEventListener('dragstart', (e) => {
draggedItem = {
element: item,
id: getItemId(item),
name: getItemName(item),
image: getItemImage(item)
};
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
item.addEventListener('dragend', (e) => {
item.classList.remove('dragging');
draggedItem = null;
});
}
// Get item ID from element
function getItemId(item) {
// Try to extract item ID from various attributes
const id = item.getAttribute('data-item') ||
item.getAttribute('data-id') ||
item.querySelector('[data-item]')?.getAttribute('data-item') ||
item.id ||
'item_' + Math.random().toString(36).substr(2, 9);
return id;
}
// Get item name from element
function getItemName(item) {
const nameSelectors = [
'.name',
'.item-name',
'.title',
'img[alt]',
'.desc'
];
for (const selector of nameSelectors) {
const element = item.querySelector(selector);
if (element) {
if (element.tagName === 'IMG') {
return element.alt || 'Unknown Item';
}
return element.textContent.trim() || 'Unknown Item';
}
}
return item.textContent.trim().substring(0, 50) || 'Unknown Item';
}
// Get item image from element
function getItemImage(item) {
const img = item.querySelector('img');
return img ? img.src : null;
}
// Render categories
function renderCategories() {
const container = document.getElementById('categories-container');
if (!container) return;
container.innerHTML = '';
// Render root level categories
const rootCategories = Object.values(categories).filter(cat => !cat.parent);
rootCategories.forEach(category => {
container.appendChild(renderCategory(category));
});
}
// Render a single category
function renderCategory(category) {
const categoryDiv = document.createElement('div');
categoryDiv.className = `category-item ${category.collapsed ? 'collapsed' : ''}`;
categoryDiv.setAttribute('data-category-id', category.id);
// Category header
const header = document.createElement('div');
header.className = 'category-header';
header.innerHTML = `
<span class="category-name">${category.name}</span>
<div class="category-controls">
<button onclick="addSubCategory('${category.id}')">+</button>
<button onclick="editCategory('${category.id}')">✎</button>
<button onclick="deleteCategory('${category.id}')">×</button>
</div>
`;
// Toggle collapse on header click
header.addEventListener('click', (e) => {
if (!e.target.matches('button')) {
toggleCategory(category.id);
}
});
categoryDiv.appendChild(header);
// Drop zone
const dropZone = document.createElement('div');
dropZone.className = 'category-drop-zone';
dropZone.textContent = 'Drop items here';
setupDropZone(dropZone, category.id);
categoryDiv.appendChild(dropZone);
// Category items
const itemsDiv = document.createElement('div');
itemsDiv.className = 'category-items';
const categoryItems = Object.entries(itemsMapping).filter(([itemId, catId]) => catId === category.id);
categoryItems.forEach(([itemId, catId]) => {
const itemPreview = document.createElement('div');
itemPreview.className = 'category-item-preview';
itemPreview.innerHTML = `
<span>${itemId}</span>
<button class="remove-item-btn" onclick="removeItemFromCategory('${itemId}')">×</button>
`;
itemsDiv.appendChild(itemPreview);
});
categoryDiv.appendChild(itemsDiv);
// Children categories
if (category.children && category.children.length > 0) {
const childrenDiv = document.createElement('div');
childrenDiv.className = 'category-children';
category.children.forEach(childId => {
if (categories[childId]) {
childrenDiv.appendChild(renderCategory(categories[childId]));
}
});
categoryDiv.appendChild(childrenDiv);
}
return categoryDiv;
}
// Setup drop zone functionality
function setupDropZone(dropZone, categoryId) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', (e) => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
if (draggedItem) {
addItemToCategory(draggedItem.id, draggedItem.name, categoryId);
}
});
}
// Add item to category
function addItemToCategory(itemId, itemName, categoryId) {
itemsMapping[itemId] = categoryId;
saveData();
renderCategories();
console.log(`[Torn Categories] Added item ${itemName} to category ${categories[categoryId].name}`);
}
// Remove item from category
window.removeItemFromCategory = function(itemId) {
delete itemsMapping[itemId];
saveData();
renderCategories();
};
// Show add category dialog
function showAddCategoryDialog(parentId = null) {
const modal = document.createElement('div');
modal.className = 'category-modal';
modal.innerHTML = `
<div class="category-modal-content">
<h3>${parentId ? 'Add Subcategory' : 'Add Category'}</h3>
<input type="text" id="category-name-input" placeholder="Category name" maxlength="50">
${parentId ? '' : `
<select id="parent-category-select">
<option value="">No parent (root level)</option>
${Object.values(categories).map(cat =>
`<option value="${cat.id}">${cat.name}</option>`
).join('')}
</select>
`}
<div class="category-modal-buttons">
<button class="torn-btn" onclick="this.closest('.category-modal').remove()">Cancel</button>
<button class="torn-btn" onclick="createCategory('${parentId || ''}')">Create</button>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('category-name-input').focus();
// Handle Enter key
document.getElementById('category-name-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
createCategory(parentId || '');
}
});
}
// Create new category
window.createCategory = function(parentId) {
const nameInput = document.getElementById('category-name-input');
const parentSelect = document.getElementById('parent-category-select');
const name = nameInput.value.trim();
if (!name) {
alert('Please enter a category name');
return;
}
const actualParentId = parentId || (parentSelect ? parentSelect.value : null);
const categoryId = 'cat_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
const newCategory = {
id: categoryId,
name: name,
parent: actualParentId || null,
children: [],
collapsed: false
};
categories[categoryId] = newCategory;
// Add to parent's children if parent exists
if (actualParentId && categories[actualParentId]) {
categories[actualParentId].children.push(categoryId);
}
saveData();
renderCategories();
document.querySelector('.category-modal').remove();
};
// Add subcategory
window.addSubCategory = function(parentId) {
showAddCategoryDialog(parentId);
};
// Edit category
window.editCategory = function(categoryId) {
const category = categories[categoryId];
if (!category) return;
const newName = prompt('Enter new category name:', category.name);
if (newName && newName.trim()) {
category.name = newName.trim();
saveData();
renderCategories();
}
};
// Delete category
window.deleteCategory = function(categoryId) {
if (categoryId === 'default') {
alert('Cannot delete the default category');
return;
}
const category = categories[categoryId];
if (!category) return;
if (!confirm(`Delete category "${category.name}" and all its subcategories?`)) {
return;
}
// Move items to default category
Object.keys(itemsMapping).forEach(itemId => {
if (itemsMapping[itemId] === categoryId) {
itemsMapping[itemId] = 'default';
}
});
// Delete recursively
deleteCategoryRecursive(categoryId);
saveData();
renderCategories();
};
// Delete category and all children recursively
function deleteCategoryRecursive(categoryId) {
const category = categories[categoryId];
if (!category) return;
// Delete children first
if (category.children) {
category.children.forEach(childId => {
deleteCategoryRecursive(childId);
});
}
// Remove from parent's children
if (category.parent && categories[category.parent]) {
const parentChildren = categories[category.parent].children;
const index = parentChildren.indexOf(categoryId);
if (index > -1) {
parentChildren.splice(index, 1);
}
}
// Delete the category
delete categories[categoryId];
}
// Toggle category collapse
function toggleCategory(categoryId) {
if (categories[categoryId]) {
categories[categoryId].collapsed = !categories[categoryId].collapsed;
saveData();
renderCategories();
}
}
// Toggle categories panel
function toggleCategoriesPanel() {
const container = document.getElementById('categories-container');
if (container) {
container.style.display = container.style.display === 'none' ? 'block' : 'none';
}
}
// Setup observer for dynamic content
function setupInventoryObserver() {
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
// Check if new inventory items were added
if (node.matches && (node.matches('.item') || node.querySelector('.item'))) {
shouldUpdate = true;
}
}
});
});
if (shouldUpdate) {
setTimeout(() => {
makeItemsDraggable();
}, 100);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Also try to initialize after a short delay for dynamic content
setTimeout(init, 1000);
})();