Torn Crime Scenario Filter with Level

Adds filters to show/hide whole crime scenario cards by level on Torn's faction crimes page with persistent settings and position

目前为 2025-02-17 提交的版本。查看 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址