// ==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*
// @match https://www.torn.com/bazaar.php*
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @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;
// Load data first
loadData();
// Check which page we're on and initialize accordingly
if (isInventoryPage()) {
console.log('[Torn Inventory] Initializing inventory management...');
createCategoryInterface();
setTimeout(() => {
console.log('[Torn Inventory] Looking for inventory items...');
setupInventoryObserver();
addInventoryControlsToItems();
}, 1000);
} else if (isBazaarPage()) {
console.log('[Torn Inventory] Initializing bazaar category display...');
setTimeout(() => {
showCategoriesOnBazaarPage();
setupBazaarObserver();
}, 2000);
} else if (isItemMarketPage()) {
console.log('[Torn Inventory] Initializing item market category display...');
setTimeout(() => {
showCategoriesOnItemMarketPage();
setupItemMarketObserver();
}, 2000);
} else {
console.log('[Torn Inventory] Not on supported page, skipping initialization');
return;
}
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);
}
// Check if current page is bazaar
function isBazaarPage() {
return window.location.href.includes('bazaar.php');
}
// Check if current page is item market
function isItemMarketPage() {
return window.location.href.includes('page.php?sid=ItemMarket');
}
// 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);
}
}
// Show categories on bazaar page
function showCategoriesOnBazaarPage() {
console.log('[Torn Inventory] Adding category labels to bazaar page...');
setTimeout(() => {
const itemElements = findItemElementsOnPage();
console.log('[Torn Inventory] Found ' + itemElements.length + ' items on bazaar page');
itemElements.forEach(element => {
const itemId = extractItemIdFromElement(element);
const itemName = extractItemNameFromElement(element);
if (itemId && itemsMapping[itemId]) {
const categoryId = itemsMapping[itemId];
const category = categories[categoryId];
if (category) {
addCategoryLabelToElement(element, category);
console.log('[Torn Inventory] Added category label "' + category.name + '" to ' + (itemName || itemId));
}
}
});
}, 500);
}
// Show categories on item market page
function showCategoriesOnItemMarketPage() {
console.log('[Torn Inventory] Adding category labels to item market page...');
setTimeout(() => {
const itemElements = findItemElementsOnPage();
console.log('[Torn Inventory] Found ' + itemElements.length + ' items on market page');
itemElements.forEach(element => {
const itemId = extractItemIdFromElement(element);
const itemName = extractItemNameFromElement(element);
if (itemId && itemsMapping[itemId]) {
const categoryId = itemsMapping[itemId];
const category = categories[categoryId];
if (category) {
addCategoryLabelToElement(element, category);
console.log('[Torn Inventory] Added category label "' + category.name + '" to ' + (itemName || itemId));
}
}
});
}, 500);
}
// Find item elements on any page
function findItemElementsOnPage() {
const selectors = [
'[data-item]',
'li[data-item]',
'div[data-item]',
'tr[data-item]'
];
const foundElements = [];
selectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
if (looksLikeItemElement(element) && !foundElements.includes(element)) {
foundElements.push(element);
}
});
} catch (e) {
// Ignore selector errors
}
});
return foundElements;
}
// Check if an element looks like it represents an item
function looksLikeItemElement(element) {
if (element.querySelector('.category-label')) {
return false;
}
const hasContent = element.textContent.trim().length > 2;
if (!hasContent) return false;
const isVisible = element.offsetWidth > 0 && element.offsetHeight > 0;
if (!isVisible) return false;
const hasReasonableSize = element.offsetWidth > 50 && element.offsetHeight > 20;
if (!hasReasonableSize) return false;
const hasImage = element.querySelector('img');
const hasDataItem = element.hasAttribute('data-item');
const hasCheckbox = element.querySelector('input[type="checkbox"]');
return hasImage || hasDataItem || hasCheckbox;
}
// Extract item ID from an element on any page
function extractItemIdFromElement(element) {
let itemId = element.getAttribute('data-item') ||
element.getAttribute('data-id') ||
element.getAttribute('data-item-id');
if (!itemId) {
const childWithId = element.querySelector('[data-item], [data-id], [data-item-id]');
if (childWithId) {
itemId = childWithId.getAttribute('data-item') ||
childWithId.getAttribute('data-id') ||
childWithId.getAttribute('data-item-id');
}
}
if (!itemId) {
let parent = element.parentElement;
let levels = 0;
while (parent && levels < 2) {
itemId = parent.getAttribute('data-item') ||
parent.getAttribute('data-id') ||
parent.getAttribute('data-item-id');
if (itemId) break;
parent = parent.parentElement;
levels++;
}
}
return itemId;
}
// Extract item name from an element on any page
function extractItemNameFromElement(element) {
const textElements = element.querySelectorAll('div, span, td, th, p');
for (const textEl of textElements) {
const text = textEl.textContent.trim();
if (text &&
text.length > 2 &&
text.length < 100 &&
!text.match(/^\d+$/) &&
!text.match(/^[\d,.\s$rrp]+$/i) &&
!text.includes('$') &&
!text.includes('RRP') &&
!text.toLowerCase().includes('qty') &&
!text.toLowerCase().includes('price') &&
!text.toLowerCase().includes('equipped') &&
!text.toLowerCase().includes('untradeable')) {
if (textEl.children.length === 0) {
return text;
}
}
}
const img = element.querySelector('img[alt]');
if (img && img.alt) {
return img.alt;
}
return null;
}
// Add a category label to an element
function addCategoryLabelToElement(element, category) {
if (element.querySelector('.category-label')) {
return;
}
const label = document.createElement('div');
label.className = 'category-label';
if (category.parent) {
label.classList.add('subcategory');
}
let categoryText = category.name;
if (category.parent && categories[category.parent]) {
categoryText = categories[category.parent].name + ' → ' + category.name;
}
label.textContent = categoryText;
label.title = 'Category: ' + categoryText;
const insertionPoint = findBestInsertionPoint(element);
if (insertionPoint) {
insertionPoint.appendChild(label);
} else {
element.style.position = 'relative';
label.style.position = 'absolute';
label.style.top = '2px';
label.style.right = '2px';
label.style.zIndex = '100';
element.appendChild(label);
}
}
// Find the best place to insert a category label
function findBestInsertionPoint(element) {
const candidates = element.querySelectorAll('td:last-child, div:last-child, div, span, td');
for (const candidate of candidates) {
const text = candidate.textContent.trim().toLowerCase();
if (text === '' ||
text === 'equipped' ||
text === 'untradeable' ||
text.includes('qty') ||
text.includes('price') ||
candidate.children.length === 0) {
return candidate;
}
}
const containerDivs = element.querySelectorAll('div');
if (containerDivs.length > 0) {
return containerDivs[containerDivs.length - 1];
}
return null;
}
// Setup observer for bazaar page
function setupBazaarObserver() {
const observer = new MutationObserver(() => {
setTimeout(showCategoriesOnBazaarPage, 1000);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('[Torn Inventory] Bazaar observer set up');
}
// Setup observer for item market page
function setupItemMarketObserver() {
const observer = new MutationObserver(() => {
setTimeout(showCategoriesOnItemMarketPage, 1000);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('[Torn Inventory] Item market observer set up');
}
// Create the category interface
function createCategoryInterface() {
const inventoryContainer = findInventoryContainer();
if (!inventoryContainer) {
console.warn('[Torn Inventory] Inventory container not found');
return;
}
const categoriesPanel = createCategoriesPanel();
inventoryContainer.parentNode.insertBefore(categoriesPanel, inventoryContainer);
renderCategories();
}
// Find the inventory container
function findInventoryContainer() {
console.log('[Torn Inventory] Searching for inventory container...');
const selectors = [
'.items-wrap',
'.inventory-wrap',
'#inventory',
'.item-list',
'.items-cont',
'.item-list-wrap',
'.your-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;
}
}
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"></div>';
addStyles();
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; } .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; } .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; } @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; } .category-label { background: #4a4a4a; color: #ddd; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-style: italic; margin: 2px 0; display: inline-block; border: 1px solid #666; } .category-label.subcategory { background: #5a5a5a; border-color: #777; } .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);
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
setTimeout(init, 1000);
})();