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.3
// @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'
        };
    }

    // Save settings to localStorage
    function saveSettings(settings) {
        Object.entries(settings).forEach(([key, value]) => {
            localStorage.setItem(STORAGE_KEYS[key.toUpperCase()], value);
        });
    }

    // Ensure position is within viewport bounds
    function keepInBounds(element) {
        const rect = element.getBoundingClientRect();
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        
        let newX = parseInt(element.style.transform.split('(')[1]) || 0;
        let newY = parseInt(element.style.transform.split(',')[1]) || 0;

        if (rect.right > viewportWidth) {
            newX = viewportWidth - rect.width;
        }
        if (rect.left < 0) {
            newX = 0;
        }
        if (rect.bottom > viewportHeight) {
            newY = viewportHeight - rect.height;
        }
        if (rect.top < 0) {
            newY = 0;
        }

        return { x: newX, y: newY };
    }

    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',
            top: '10px',
            right: '10px',
            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',
            transition: 'transform 0.3s ease'
        });

        // Create header with title and collapse button
        const headerDiv = document.createElement('div');
        headerDiv.style.display = 'flex';
        headerDiv.style.justifyContent = 'space-between';
        headerDiv.style.alignItems = 'center';
        headerDiv.style.marginBottom = '8px';
        headerDiv.style.cursor = 'move';

        const titleSpan = document.createElement('span');
        titleSpan.textContent = 'Crime Filter';
        titleSpan.style.fontWeight = 'bold';

        const collapseButton = document.createElement('button');
        collapseButton.textContent = settings.isCollapsed ? '▼' : '▲';
        collapseButton.style.background = 'none';
        collapseButton.style.border = 'none';
        collapseButton.style.color = '#fff';
        collapseButton.style.cursor = 'pointer';
        collapseButton.style.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.display = 'flex';
        controlsDiv.style.alignItems = 'center';
        controlsDiv.style.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.width = '45px';
        levelInput.style.marginLeft = '5px';
        levelInput.style.padding = '2px 4px';

        // Create status text
        const statusEl = document.createElement('div');
        statusEl.id = 'filterStatus';
        statusEl.style.fontSize = '12px';
        statusEl.style.color = '#aaa';
        statusEl.style.marginTop = '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);

        // Set initial position from saved settings
        filterDiv.style.transform = `translate(${settings.positionX}px, ${settings.positionY}px)`;

        // 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);

        // Make the filter div draggable
        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;
        let xOffset = settings.positionX;
        let yOffset = settings.positionY;

        function dragStart(e) {
            if (e.target === headerDiv || e.target === titleSpan) {
                isDragging = true;
                initialX = e.clientX - xOffset;
                initialY = e.clientY - yOffset;
            }
        }

        function drag(e) {
            if (isDragging) {
                e.preventDefault();
                currentX = e.clientX - initialX;
                currentY = e.clientY - initialY;

                xOffset = currentX;
                yOffset = currentY;

                filterDiv.style.transform = `translate(${currentX}px, ${currentY}px)`;
            }
        }

        function dragEnd() {
            if (isDragging) {
                isDragging = false;
                
                // Keep the box in bounds
                const bounds = keepInBounds(filterDiv);
                xOffset = bounds.x;
                yOffset = bounds.y;
                
                filterDiv.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
                
                // Save the new position
                saveSettings({
                    positionX: xOffset,
                    positionY: yOffset
                });
            }
        }

        headerDiv.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        // Handle window resize to keep the box in bounds
        window.addEventListener('resize', () => {
            const bounds = keepInBounds(filterDiv);
            xOffset = bounds.x;
            yOffset = bounds.y;
            filterDiv.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
            saveSettings({
                positionX: xOffset,
                positionY: yOffset
            });
        });

        // 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或关注我们的公众号极客氢云获取最新地址