Zed Champions Auction Filter and Sort

Filter auctions by star rating and sort by time remaining on Zed Champions

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Zed Champions Auction Filter and Sort
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Filter auctions by star rating and sort by time remaining on Zed Champions
// @author       You
// @match        https://app.zedchampions.com/auctions
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zedchampions.com
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const REFRESH_INTERVAL = 1000; // Check for new auctions every 1 second
    const DEBOUNCE_TIME = 300; // Debounce time in ms for handling multiple rapid changes
    const STATUS_HIDE_DELAY = 2000; // Hide status message after 2 seconds of no changes
    const STYLE = `
        .zed-filter-container {
            position: sticky;
            top: 0;
            background-color: #1A202C;
            padding: 12px 20px;
            z-index: 1000;
            display: flex;
            flex-wrap: wrap;
            gap: 10px 20px;
            border-bottom: 1px solid #2D3748;
            align-items: center;
        }
        .zed-filter-row {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            flex: 1;
            align-items: center;
        }
        .zed-filter-group {
            display: flex;
            flex-direction: column;
            gap: 6px;
        }
        .zed-filter-group label {
            font-weight: bold;
            color: white;
            font-size: 13px;
            white-space: nowrap;
        }
        .zed-filter-buttons {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
        }
        .zed-filter-btn {
            background-color: #2D3748;
            color: white;
            border: none;
            padding: 6px 10px;
            border-radius: 4px;
            cursor: pointer;
            min-width: 40px;
            text-align: center;
            font-size: 13px;
            transition: background-color 0.2s;
        }
        .zed-filter-btn.active {
            background-color: #4299E1;
        }
        .zed-bloodline-btn {
            min-width: 90px;
        }
        .zed-controls-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            align-items: center;
            justify-content: flex-end;
            margin-left: auto;
        }
        .zed-controls-group button {
            margin: 0;
        }
        .zed-sort-btn {
            background-color: #2D3748;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            font-size: 13px;
            white-space: nowrap;
        }
        .zed-sort-btn.active {
            background-color: #4299E1;
        }
        .zed-reset-btn {
            background-color: #E53E3E;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            font-size: 13px;
            white-space: nowrap;
        }
        .zed-card-hidden {
            display: none !important;
        }
        .zed-status-box {
            background-color: rgba(74, 85, 104, 0.9);
            color: white;
            padding: 8px 15px;
            border-radius: 4px;
            font-size: 13px;
            font-weight: bold;
            display: none;
            align-self: flex-start;
            min-width: 160px;
            text-align: center;
            animation: fadeIn 0.3s ease;
        }
        .zed-status-container {
            display: flex;
            align-items: center;
            margin-left: auto;
            margin-right: 20px;
        }
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
    `;

    // State
    let activeStarFilters = [];
    let activeBloodlineFilters = [];
    let sortByTimeAsc = false;
    let isProcessing = false;
    let debounceTimer = null;
    let processQueue = 0;
    let lastTotalAuctions = 0;
    let statusHideTimer = null;
    let statusElement = null;
    let isScanning = false;

    // Add a debounce function to prevent excessive processing
    function debounce(func, wait) {
        return function() {
            const context = this;
            const args = arguments;
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                func.apply(context, args);
            }, wait);
        };
    }

    // Core processing logic
    function processAuctionsCore() {
        // Store all cards before we make any changes
        const auctionCards = document.querySelectorAll('.css-4k37cb, [class*="auction-card"]');
        const totalAuctions = auctionCards.length;

        // Check if total auctions has changed
        if (totalAuctions !== lastTotalAuctions) {
            // Show scanning message only when total count changes
            showScanningMessage(totalAuctions);
            lastTotalAuctions = totalAuctions;
            isScanning = true;
        }

        // Store original positions if not already stored
        auctionCards.forEach((card, index) => {
            if (!card.dataset.originalIndex) {
                card.dataset.originalIndex = index;
            }
        });

        // Apply filters
        auctionCards.forEach(card => {
            const starRating = getStarRating(card);
            const bloodline = getBloodline(card);

            // Check if filters are active
            const hasStarFilters = activeStarFilters.length > 0;
            const hasBloodlineFilters = activeBloodlineFilters.length > 0;

            // Star rating filter check
            const matchesStarFilter = !hasStarFilters || activeStarFilters.includes(starRating.toString());

            // Bloodline filter check
            const matchesBloodlineFilter = !hasBloodlineFilters || (bloodline && activeBloodlineFilters.includes(bloodline));

            // Apply combined filters (AND logic)
            card.classList.toggle('zed-card-hidden', !(matchesStarFilter && matchesBloodlineFilter));
        });

        // Apply sorting if enabled
        if (sortByTimeAsc) {
            sortCardsByTimeRemaining();
        }

        // Count visible auctions
        const visibleAuctions = document.querySelectorAll('.css-4k37cb:not(.zed-card-hidden), [class*="auction-card"]:not(.zed-card-hidden)').length;

        // Update status message with count
        updateStatusMessage(`${visibleAuctions} out of ${totalAuctions} auctions`);

        // Reset the hide timer since we've updated the message
        resetStatusHideTimer();
    }

    // Show scanning message
    function showScanningMessage(totalAuctions) {
        if (!statusElement) {
            return;
        }

        statusElement.textContent = `Scanning ${totalAuctions} auctions...`;
        statusElement.style.display = 'block';
    }

    // Show status message
    function showStatusMessage(message) {
        if (!statusElement) {
            return;
        }

        statusElement.textContent = message;
        statusElement.style.display = 'block';
    }

    // Update status message
    function updateStatusMessage(message) {
        if (!statusElement) {
            return;
        }

        statusElement.textContent = message;
        statusElement.style.display = 'block';
    }

    // Reset the timer that hides the status message
    function resetStatusHideTimer() {
        if (statusHideTimer) {
            clearTimeout(statusHideTimer);
        }

        // Only hide the scanning message after delay
        // Keep the auction count visible
        statusHideTimer = setTimeout(() => {
            if (statusElement && isScanning) {
                isScanning = false;

                // Get current counts to display after scanning
                const totalAuctions = document.querySelectorAll('.css-4k37cb, [class*="auction-card"]').length;
                const visibleAuctions = document.querySelectorAll('.css-4k37cb:not(.zed-card-hidden), [class*="auction-card"]:not(.zed-card-hidden)').length;

                // Update the message
                updateStatusMessage(`${visibleAuctions} out of ${totalAuctions} auctions`);
            }
        }, STATUS_HIDE_DELAY);
    }

    // Enhanced process auctions function with debounce
    const debouncedProcessAuctions = debounce(() => {
        // If we're already processing, increment the queue
        if (isProcessing) {
            processQueue++;
            return;
        }

        isProcessing = true;

        try {
            // Actual processing logic
            processAuctionsCore();
        } finally {
            isProcessing = false;

            // If we have queued processing requests, process again
            if (processQueue > 0) {
                processQueue = 0;
                setTimeout(debouncedProcessAuctions, 50);
            }
        }
    }, DEBOUNCE_TIME);

    // Toggle bloodline filter
    function toggleBloodlineFilter(event) {
        const bloodlineValue = event.target.dataset.bloodline;

        if (activeBloodlineFilters.includes(bloodlineValue)) {
            // Remove filter
            activeBloodlineFilters = activeBloodlineFilters.filter(b => b !== bloodlineValue);
            event.target.classList.remove('active');
        } else {
            // Add filter
            activeBloodlineFilters.push(bloodlineValue);
            event.target.classList.add('active');
        }

        debouncedProcessAuctions();
    }

    // Toggle star rating filter
    function toggleStarFilter(event) {
        const starValue = event.target.dataset.stars;

        if (activeStarFilters.includes(starValue)) {
            // Remove filter
            activeStarFilters = activeStarFilters.filter(s => s !== starValue);
            event.target.classList.remove('active');
        } else {
            // Add filter
            activeStarFilters.push(starValue);
            event.target.classList.add('active');
        }

        debouncedProcessAuctions();
    }

    // Toggle time sort
    function toggleTimeSort(event) {
        sortByTimeAsc = !sortByTimeAsc;

        // Update button text
        event.target.textContent = sortByTimeAsc ? 'Sort by Time ↓' : 'Sort by Time ↑';
        event.target.classList.toggle('active', sortByTimeAsc);

        debouncedProcessAuctions();
    }

    // Reset all filters and sorting
    function resetFilters() {
        // Clear star filters
        activeStarFilters = [];
        document.querySelectorAll('.zed-filter-btn[data-stars]').forEach(btn => {
            btn.classList.remove('active');
        });

        // Clear bloodline filters
        activeBloodlineFilters = [];
        document.querySelectorAll('.zed-filter-btn[data-bloodline]').forEach(btn => {
            btn.classList.remove('active');
        });

        // Reset sort
        sortByTimeAsc = false;
        const sortBtn = document.querySelector('.zed-sort-btn');
        if (sortBtn) {
            sortBtn.textContent = 'Sort by Time ↑';
            sortBtn.classList.remove('active');
        }

        // Reset visibility of cards
        document.querySelectorAll('.css-4k37cb').forEach(card => {
            card.classList.remove('zed-card-hidden');
        });

        // Reset sort order
        const auctionContainer = document.querySelector('#auctionlist-container') ||
                                document.querySelector('.css-1v5qfvc') ||
                                document.querySelector('.css-14v29z6') ||
                                document.querySelector('.css-1tvu0bo');

        if (auctionContainer) {
            // Sort back to original order
            const cards = Array.from(auctionContainer.querySelectorAll('.css-4k37cb'));
            cards.sort((a, b) => {
                return parseInt(a.dataset.originalIndex || 0) - parseInt(b.dataset.originalIndex || 0);
            });

            // Re-append in original order
            cards.forEach(card => {
                auctionContainer.appendChild(card);
            });
        }

        // Show message after reset
        const totalAuctions = document.querySelectorAll('.css-4k37cb, [class*="auction-card"]').length;
        updateStatusMessage(`${totalAuctions} out of ${totalAuctions} auctions`);

        // Process auctions to update status
        debouncedProcessAuctions();
    }

    // Create the filter UI
    function createFilterUI() {
        const container = document.createElement('div');
        container.className = 'zed-filter-container';

        // Create a filter row for all filters
        const filterRow = document.createElement('div');
        filterRow.className = 'zed-filter-row';
        container.appendChild(filterRow);

        // Star rating filter group
        const starFilterGroup = document.createElement('div');
        starFilterGroup.className = 'zed-filter-group';

        const starLabel = document.createElement('label');
        starLabel.textContent = 'Filter by Star Rating:';
        starFilterGroup.appendChild(starLabel);

        const starButtons = document.createElement('div');
        starButtons.className = 'zed-filter-buttons';

        // Create buttons for whole and half star ratings from 1 to 5
        for (let i = 1; i <= 5; i++) {
            // Whole number
            const wholeBtn = document.createElement('button');
            wholeBtn.className = 'zed-filter-btn';
            wholeBtn.textContent = i.toString();
            wholeBtn.dataset.stars = i.toString();
            wholeBtn.addEventListener('click', toggleStarFilter);
            starButtons.appendChild(wholeBtn);

            // Half star
            if (i < 5) {
                const halfBtn = document.createElement('button');
                halfBtn.className = 'zed-filter-btn';
                halfBtn.textContent = (i + 0.5).toString();
                halfBtn.dataset.stars = (i + 0.5).toString();
                halfBtn.addEventListener('click', toggleStarFilter);
                starButtons.appendChild(halfBtn);
            }
        }

        starFilterGroup.appendChild(starButtons);
        filterRow.appendChild(starFilterGroup);

        // Bloodline filter group
        const bloodlineFilterGroup = document.createElement('div');
        bloodlineFilterGroup.className = 'zed-filter-group';

        const bloodlineLabel = document.createElement('label');
        bloodlineLabel.textContent = 'Filter by Bloodline:';
        bloodlineFilterGroup.appendChild(bloodlineLabel);

        const bloodlineButtons = document.createElement('div');
        bloodlineButtons.className = 'zed-filter-buttons';

        // Create buttons for each bloodline
        const bloodlines = ['NAKAMOTO', 'SZABO', 'FINNEY', 'BUTERIN'];
        bloodlines.forEach(bloodline => {
            const btn = document.createElement('button');
            btn.className = 'zed-filter-btn zed-bloodline-btn';
            btn.textContent = bloodline;
            btn.dataset.bloodline = bloodline;
            btn.addEventListener('click', toggleBloodlineFilter);
            bloodlineButtons.appendChild(btn);
        });

        bloodlineFilterGroup.appendChild(bloodlineButtons);
        filterRow.appendChild(bloodlineFilterGroup);

        // Status container for better positioning
        const statusContainer = document.createElement('div');
        statusContainer.className = 'zed-status-container';

        // Status message box
        statusElement = document.createElement('div');
        statusElement.className = 'zed-status-box';
        statusElement.textContent = 'Ready';
        statusContainer.appendChild(statusElement);

        // Add status container to filter row
        filterRow.appendChild(statusContainer);

        // Controls group (for sort/reset buttons)
        const controlsGroup = document.createElement('div');
        controlsGroup.className = 'zed-controls-group';

        // Sort by time button
        const sortBtn = document.createElement('button');
        sortBtn.className = 'zed-sort-btn';
        sortBtn.textContent = 'Sort by Time ↑';
        sortBtn.addEventListener('click', toggleTimeSort);
        controlsGroup.appendChild(sortBtn);

        // Reset button
        const resetBtn = document.createElement('button');
        resetBtn.className = 'zed-reset-btn';
        resetBtn.textContent = 'Reset All';
        resetBtn.addEventListener('click', resetFilters);
        controlsGroup.appendChild(resetBtn);

        container.appendChild(controlsGroup);

        // Insert the filter container at the top of the page
        const targetElement = document.querySelector('main') || document.body;
        targetElement.insertBefore(container, targetElement.firstChild);
    }

    // Function to get the star rating from an auction card
    function getStarRating(card) {
        // Find all star elements in the card
        const starContainer = card.querySelector('.css-k008qs, [class*="star-rating"]');
        if (!starContainer) return 0;

        // First try the new UI format (count filled stars)
        const starElements = starContainer.querySelectorAll('*');
        let rating = 0;

        if (starElements.length > 0) {
            // Try to count filled stars from text content
            const ratingText = starContainer.textContent.trim();
            // Look for patterns like "★★★☆☆" or "3.5" or "★★★½"
            if (ratingText.match(/^[\d\.]+$/)) {
                // If it's a numeric rating (e.g., "3.5")
                rating = parseFloat(ratingText);
            } else if (ratingText.includes('★')) {
                // Count full stars
                rating = (ratingText.match(/★/g) || []).length;
                // Check for half star (½ or ☆)
                if (ratingText.includes('½') || (ratingText.includes('☆') && rating > 0)) {
                    rating += 0.5;
                }
            } else {
                // Fall back to original method for the old UI
                let fullStars = 0;
                let hasHalfStar = false;

                // Look for star containers with css-1o988h9 class
                const oldStarElements = starContainer.querySelectorAll('.css-1o988h9');

                oldStarElements.forEach(starElement => {
                    // If it has a fully filled star (css-1tt5dvj)
                    if (starElement.querySelector('.css-1tt5dvj')) {
                        fullStars++;
                    }
                    // If it has a half-filled star (css-139eomv)
                    else if (starElement.querySelector('.css-139eomv')) {
                        hasHalfStar = true;
                    }
                });

                rating = fullStars + (hasHalfStar ? 0.5 : 0);
            }
        }

        return rating;
    }

    // Function to get the bloodline from an auction card
    function getBloodline(card) {
        // Look for text that might contain a bloodline
        const textElements = card.querySelectorAll('p, div, span');
        const bloodlines = ['NAKAMOTO', 'SZABO', 'FINNEY', 'BUTERIN'];

        for (const element of textElements) {
            const text = element.textContent.trim().toUpperCase();
            for (const bloodline of bloodlines) {
                if (text === bloodline) {
                    return bloodline;
                }
            }
        }

        return null;
    }

    // Function to get time remaining in minutes from an auction card
    function getTimeRemainingInMinutes(card) {
        // Find the time element using multiple possible selectors
        const timeElement = card.querySelector('.css-1baulvz, .css-1fb8s8w span, [class*="time-remaining"], div[class*="Time"] span');
        if (!timeElement) return Infinity;

        const timeText = timeElement.textContent.trim();

        // Parse the time string (format: "1d 3h 5m 30s" or similar)
        let days = 0, hours = 0, minutes = 0, seconds = 0;

        // Check for days
        if (timeText.includes('d')) {
            const dayMatch = timeText.match(/(\d+)d/);
            if (dayMatch && dayMatch[1]) {
                days = parseInt(dayMatch[1]) || 0;
            }
        }

        // Check for hours
        if (timeText.includes('h')) {
            const hourMatch = timeText.match(/(\d+)h/);
            if (hourMatch && hourMatch[1]) {
                hours = parseInt(hourMatch[1]) || 0;
            }
        }

        // Check for minutes
        if (timeText.includes('m')) {
            const minuteMatch = timeText.match(/(\d+)m/);
            if (minuteMatch && minuteMatch[1]) {
                minutes = parseInt(minuteMatch[1]) || 0;
            }
        }

        // Check for seconds
        if (timeText.includes('s')) {
            const secondMatch = timeText.match(/(\d+)s/);
            if (secondMatch && secondMatch[1]) {
                seconds = parseInt(secondMatch[1]) || 0;
            }
        }

        // Convert to total minutes (including a fraction for seconds)
        return (days * 24 * 60) + (hours * 60) + minutes + (seconds / 60);
    }

    // Function to sort cards by time remaining
    function sortCardsByTimeRemaining() {
        // Find auction container
        const auctionContainer = document.querySelector('#auctionlist-container') ||
                                document.querySelector('.css-1v5qfvc') ||
                                document.querySelector('.css-14v29z6') ||
                                document.querySelector('.css-1tvu0bo');

        if (!auctionContainer) {
            console.error('Auction container not found. Unable to sort cards.');
            return;
        }

        // Process sorting with the found container
        processSort(auctionContainer);

        // Function to handle the actual sorting logic
        function processSort(container) {
            const cards = Array.from(container.querySelectorAll('.css-4k37cb'))
                .filter(card => !card.classList.contains('zed-card-hidden'));

            if (cards.length === 0) {
                return; // Don't proceed if no cards
            }

            cards.sort((a, b) => {
                const timeA = getTimeRemainingInMinutes(a);
                const timeB = getTimeRemainingInMinutes(b);
                return timeA - timeB; // Low to high
            });

            // Re-append sorted cards
            cards.forEach(card => {
                container.appendChild(card);
            });
        }
    }

    // Initialize the script
    function init() {
        // Check if we're on the right page
        if (window.location.href !== "https://app.zedchampions.com/auctions") {
            console.log('Not on the auctions page, skipping initialization');
            return;
        }

        // Add styles
        addStyles();

        // Add filter UI
        createFilterUI();

        // Initial processing
        debouncedProcessAuctions();

        // Set up observer for new auctions
        const targetNode = document.body;

        const observer = new MutationObserver(() => {
            debouncedProcessAuctions();
        });

        // Start observing
        observer.observe(targetNode, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class']
        });

        // Also set up regular refreshes as fallback
        setInterval(debouncedProcessAuctions, REFRESH_INTERVAL);
    }

    // Add styles to the page
    function addStyles() {
        const styleElement = document.createElement('style');
        styleElement.textContent = STYLE;
        document.head.appendChild(styleElement);
    }

    // Wait for page to load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();