// ==UserScript==
// @name Torn Crime Scenario Filter with Level
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Adds filters to show/hide whole crime scenario cards by level on Torn's faction crimes page with persistent settings and position
// @match https://www.torn.com/factions.php*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Constants for localStorage keys
const STORAGE_KEYS = {
SHOW: 'tornCrimeFilter_show',
LEVEL: 'tornCrimeFilter_level',
POSITION_X: 'tornCrimeFilter_posX',
POSITION_Y: 'tornCrimeFilter_posY',
IS_COLLAPSED: 'tornCrimeFilter_collapsed'
};
// Default settings
const DEFAULT_SETTINGS = {
show: true,
level: 1,
positionX: 0,
positionY: 0,
isCollapsed: false
};
// Load saved settings or use defaults
function loadSettings() {
return {
show: localStorage.getItem(STORAGE_KEYS.SHOW) !== 'false',
level: parseInt(localStorage.getItem(STORAGE_KEYS.LEVEL)) || DEFAULT_SETTINGS.level,
positionX: parseInt(localStorage.getItem(STORAGE_KEYS.POSITION_X)) || DEFAULT_SETTINGS.positionX,
positionY: parseInt(localStorage.getItem(STORAGE_KEYS.POSITION_Y)) || DEFAULT_SETTINGS.positionY,
isCollapsed: localStorage.getItem(STORAGE_KEYS.IS_COLLAPSED) === 'true'
};
}
// Debounce function for performance optimization
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Save settings to localStorage (debounced to improve performance)
const saveSettings = debounce((settings) => {
Object.entries(settings).forEach(([key, value]) => {
localStorage.setItem(STORAGE_KEYS[key.toUpperCase()], value);
});
}, 250);
// Ensure position is within viewport bounds
function keepInBounds(x, y, elementRect, viewportWidth, viewportHeight) {
return {
x: Math.min(Math.max(x, 0), viewportWidth - elementRect.width),
y: Math.min(Math.max(y, 0), viewportHeight - elementRect.height)
};
}
function updateFilters() {
const showCheckbox = document.getElementById('crimeFilterCheckbox');
const levelInput = document.getElementById('minCrimeLevelInput');
const shouldShow = showCheckbox.checked;
const minLevel = parseInt(levelInput.value, 10) || 1;
// Save filter settings
saveSettings({
show: shouldShow,
level: minLevel
});
// Update status text
const statusEl = document.getElementById('filterStatus');
let visibleCount = 0;
let totalCount = 0;
document.querySelectorAll('.wrapper___U2Ap7').forEach(card => {
const levelEl = card.querySelector('.level___sBl49');
totalCount++;
if (levelEl) {
const text = levelEl.textContent.trim();
const parts = text.split('/');
const level = parseInt(parts[0], 10) || 0;
if (!shouldShow || level < minLevel) {
card.style.display = 'none';
} else {
card.style.display = '';
visibleCount++;
}
} else {
card.style.display = shouldShow ? '' : 'none';
if (shouldShow) visibleCount++;
}
});
statusEl.textContent = `Showing ${visibleCount} of ${totalCount} crimes`;
}
window.addEventListener('load', function() {
const crimesContainer = document.getElementById('faction-crimes');
if (!crimesContainer) return;
// Load saved settings
const settings = loadSettings();
// Create filter UI container
const filterDiv = document.createElement('div');
Object.assign(filterDiv.style, {
position: 'fixed',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
padding: '12px',
zIndex: '10000',
borderRadius: '8px',
fontFamily: 'Arial, sans-serif',
fontSize: '14px',
boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
display: 'flex',
flexDirection: 'column',
gap: '8px',
transform: `translate(${settings.positionX}px, ${settings.positionY}px)`,
willChange: 'transform',
touchAction: 'none',
userSelect: 'none'
});
// Create header with title and collapse button
const headerDiv = document.createElement('div');
headerDiv.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
cursor: move;
touch-action: none;
`;
const titleSpan = document.createElement('span');
titleSpan.textContent = 'Crime Filter';
titleSpan.style.fontWeight = 'bold';
const collapseButton = document.createElement('button');
collapseButton.textContent = settings.isCollapsed ? '▼' : '▲';
collapseButton.style.cssText = `
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 0 5px;
`;
// Create content container
const contentDiv = document.createElement('div');
contentDiv.style.display = settings.isCollapsed ? 'none' : 'block';
// Create controls container
const controlsDiv = document.createElement('div');
controlsDiv.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
`;
// Create checkbox group
const checkboxGroup = document.createElement('div');
checkboxGroup.style.display = 'flex';
checkboxGroup.style.alignItems = 'center';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'crimeFilterCheckbox';
checkbox.checked = settings.show;
checkbox.style.marginRight = '5px';
const checkboxLabel = document.createElement('label');
checkboxLabel.htmlFor = 'crimeFilterCheckbox';
checkboxLabel.textContent = 'Show Crime Cards';
// Create level input group
const levelGroup = document.createElement('div');
levelGroup.style.display = 'flex';
levelGroup.style.alignItems = 'center';
const levelLabel = document.createElement('label');
levelLabel.htmlFor = 'minCrimeLevelInput';
levelLabel.textContent = 'Min Level:';
const levelInput = document.createElement('input');
levelInput.type = 'number';
levelInput.id = 'minCrimeLevelInput';
levelInput.min = '1';
levelInput.max = '10';
levelInput.value = settings.level;
levelInput.style.cssText = `
width: 45px;
margin-left: 5px;
padding: 2px 4px;
`;
// Create status text
const statusEl = document.createElement('div');
statusEl.id = 'filterStatus';
statusEl.style.cssText = `
font-size: 12px;
color: #aaa;
margin-top: 4px;
`;
// Assemble the UI
headerDiv.appendChild(titleSpan);
headerDiv.appendChild(collapseButton);
checkboxGroup.appendChild(checkbox);
checkboxGroup.appendChild(checkboxLabel);
levelGroup.appendChild(levelLabel);
levelGroup.appendChild(levelInput);
controlsDiv.appendChild(checkboxGroup);
controlsDiv.appendChild(levelGroup);
contentDiv.appendChild(controlsDiv);
contentDiv.appendChild(statusEl);
filterDiv.appendChild(headerDiv);
filterDiv.appendChild(contentDiv);
document.body.appendChild(filterDiv);
// Collapse button functionality
collapseButton.addEventListener('click', (e) => {
e.stopPropagation();
settings.isCollapsed = !settings.isCollapsed;
contentDiv.style.display = settings.isCollapsed ? 'none' : 'block';
collapseButton.textContent = settings.isCollapsed ? '▼' : '▲';
saveSettings({ isCollapsed: settings.isCollapsed });
});
// Add event listeners for filters
checkbox.addEventListener('change', updateFilters);
levelInput.addEventListener('input', updateFilters);
levelInput.addEventListener('change', updateFilters);
// Optimized dragging functionality
let isDragging = false;
let startX, startY;
let originalX = settings.positionX;
let originalY = settings.positionY;
const filterRect = filterDiv.getBoundingClientRect();
function onPointerDown(e) {
if (e.target === headerDiv || e.target === titleSpan) {
isDragging = true;
startX = e.clientX - originalX;
startY = e.clientY - originalY;
filterDiv.setPointerCapture(e.pointerId);
}
}
function onPointerMove(e) {
if (!isDragging) return;
// Calculate new position
let newX = e.clientX - startX;
let newY = e.clientY - startY;
// Apply bounds checking
const bounds = keepInBounds(
newX,
newY,
filterRect,
window.innerWidth,
window.innerHeight
);
// Update position using transform
originalX = bounds.x;
originalY = bounds.y;
filterDiv.style.transform = `translate(${bounds.x}px, ${bounds.y}px)`;
}
function onPointerUp(e) {
if (!isDragging) return;
isDragging = false;
filterDiv.releasePointerCapture(e.pointerId);
// Save the final position
saveSettings({
positionX: originalX,
positionY: originalY
});
}
// Use Pointer events for better performance
headerDiv.addEventListener('pointerdown', onPointerDown);
filterDiv.addEventListener('pointermove', onPointerMove);
filterDiv.addEventListener('pointerup', onPointerUp);
filterDiv.addEventListener('pointercancel', onPointerUp);
// Handle window resize (debounced)
const handleResize = debounce(() => {
const bounds = keepInBounds(
originalX,
originalY,
filterRect,
window.innerWidth,
window.innerHeight
);
originalX = bounds.x;
originalY = bounds.y;
filterDiv.style.transform = `translate(${bounds.x}px, ${bounds.y}px)`;
saveSettings({
positionX: bounds.x,
positionY: bounds.y
});
}, 100);
window.addEventListener('resize', handleResize);
// Initial filter application
updateFilters();
// Set up observer for dynamic content
const observer = new MutationObserver(updateFilters);
observer.observe(crimesContainer, { childList: true, subtree: true });
});
})();