Hide Saved Assets on FAB

hide saved assets on FAB (Unreal Engine)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Hide Saved Assets on FAB
// @namespace    https://github.com/creosB
// @version      2.1
// @description  hide saved assets on FAB (Unreal Engine)
// @author       CreosB
// @match        https://www.fab.com/channels/unreal-engine*
// @license     This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // State management
    const state = {
        isHidingEnabled: true,
        processedElements: new WeakSet(),
        hiddenElements: new Set(),
        isProcessing: false,
        lastProcessTime: 0,
        observer: null
    };

    // Performance optimizations
    const THROTTLE_DELAY = 250; // ms
    const BATCH_SIZE = 50;
    const IDLE_TIMEOUT = 100; // ms

    // Throttle function to prevent excessive calls
    function throttle(func, delay) {
        let timeoutId;
        let lastExecTime = 0;
        
        return function (...args) {
            const currentTime = Date.now();
            
            if (currentTime - lastExecTime > delay) {
                func.apply(this, args);
                lastExecTime = currentTime;
            } else {
                clearTimeout(timeoutId);
                timeoutId = setTimeout(() => {
                    func.apply(this, args);
                    lastExecTime = Date.now();
                }, delay - (currentTime - lastExecTime));
            }
        };
    }

    // Debounce function for batch processing
    function debounce(func, delay) {
        let timeoutId;
        return function (...args) {
            clearTimeout(timeoutId);
            timeoutId = setTimeout(() => func.apply(this, args), delay);
        };
    }

    // Enhanced asset processing with more aggressive detection
    function processAssets() {
        if (state.isProcessing) return;
        
        state.isProcessing = true;
        const startTime = performance.now();

        try {
            // Get all potential saved asset containers with multiple selectors
            const selectors = [
                'div.fabkit-Stack-root.nTa5u2sc',
                'div.fabkit-Stack-root[class*="nTa5u2sc"]',
                'div[class*="fabkit-Stack-root"][class*="nTa5u2sc"]'
            ];
            
            let allCards = new Set();
            selectors.forEach(selector => {
                const elements = document.querySelectorAll(selector);
                elements.forEach(el => allCards.add(el));
            });
            
            const newCards = Array.from(allCards).filter(card => !state.processedElements.has(card));
            
            console.log(`Processing ${newCards.length} new cards out of ${allCards.size} total cards`);
            
            if (newCards.length === 0) {
                state.isProcessing = false;
                return;
            }

            // Process in batches to avoid blocking the UI
            const processBatch = (batch) => {
                batch.forEach(card => {
                    state.processedElements.add(card);
                    
                    // Multiple ways to detect saved status
                    let isSaved = false;
                    
                    // Method 1: Look for success intent typography
                    const savedIndicator = card.querySelector('div.fabkit-Typography-root[class*="intent-success"]');
                    if (savedIndicator && savedIndicator.textContent.includes('Saved in My Library')) {
                        isSaved = true;
                    }
                    
                    // Method 2: Look for check-circle icon
                    if (!isSaved) {
                        const checkIcon = card.querySelector('i.edsicon-check-circle-filled');
                        if (checkIcon) {
                            const parentDiv = checkIcon.closest('div');
                            if (parentDiv && parentDiv.textContent.includes('Saved in My Library')) {
                                isSaved = true;
                            }
                        }
                    }
                    
                    // Method 3: General text search
                    if (!isSaved) {
                        const textElements = card.querySelectorAll('div, span, p');
                        for (let textEl of textElements) {
                            if (textEl.textContent && textEl.textContent.includes('Saved in My Library')) {
                                isSaved = true;
                                break;
                            }
                        }
                    }
                    
                    if (isSaved) {
                        // Store reference and current display state
                        const cardId = card.dataset.cardId || `card-${Date.now()}-${Math.random()}`;
                        card.dataset.cardId = cardId;
                        card.dataset.originalDisplay = card.style.display || '';
                        
                        state.hiddenElements.add(cardId);
                        
                        // Apply visibility based on current state
                        if (state.isHidingEnabled) {
                            card.style.display = 'none';
                            card.dataset.hiddenByScript = 'true';
                        }
                        
                        console.log('Found and processed saved asset:', cardId);
                    }
                });
            };

            // Process batches with requestIdleCallback for better performance
            let batchIndex = 0;
            const processBatches = () => {
                if (batchIndex >= newCards.length) {
                    state.isProcessing = false;
                    console.log(`Finished processing all batches. Total hidden: ${state.hiddenElements.size}`);
                    return;
                }

                const batch = newCards.slice(batchIndex, batchIndex + BATCH_SIZE);
                processBatch(batch);
                batchIndex += BATCH_SIZE;

                // Use requestIdleCallback if available, otherwise setTimeout
                if (window.requestIdleCallback) {
                    requestIdleCallback(processBatches, { timeout: IDLE_TIMEOUT });
                } else {
                    setTimeout(processBatches, 0);
                }
            };

            processBatches();

        } catch (error) {
            console.error('Error processing assets:', error);
            state.isProcessing = false;
        }
    }

    // Throttled version of processAssets
    const throttledProcessAssets = throttle(processAssets, THROTTLE_DELAY);

    // Toggle visibility of saved assets
    function toggleSavedAssets() {
        state.isHidingEnabled = !state.isHidingEnabled;
        
        // Update all tracked hidden elements
        state.hiddenElements.forEach(cardId => {
            const card = document.querySelector(`[data-card-id="${cardId}"]`);
            if (card) {
                if (state.isHidingEnabled) {
                    card.style.display = 'none';
                    card.dataset.hiddenByScript = 'true';
                } else {
                    card.style.display = card.dataset.originalDisplay || '';
                    card.dataset.hiddenByScript = 'false';
                }
            }
        });

        return state.isHidingEnabled;
    }

    // Enhanced mutation observer with multiple detection strategies
    function setupObserver() {
        if (state.observer) {
            state.observer.disconnect();
        }

        state.observer = new MutationObserver(throttle((mutations) => {
            let shouldProcess = false;
            
            mutations.forEach(mutation => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // Check if any added nodes contain our target elements
                    const hasRelevantNodes = Array.from(mutation.addedNodes).some(node => {
                        if (node.nodeType !== 1) return false;
                        
                        return (
                            node.matches('div.fabkit-Stack-root.nTa5u2sc') ||
                            node.querySelector('div.fabkit-Stack-root.nTa5u2sc') ||
                            node.matches('div.fabkit-Stack-root') ||
                            node.querySelector('div.fabkit-Stack-root') ||
                            node.matches('[class*="fabkit"]') ||
                            node.querySelector('[class*="fabkit"]')
                        );
                    });
                    
                    if (hasRelevantNodes) {
                        shouldProcess = true;
                    }
                }
                
                // Also check for attribute changes that might indicate new content
                if (mutation.type === 'attributes' && 
                    (mutation.attributeName === 'style' || 
                     mutation.attributeName === 'class' ||
                     mutation.attributeName === 'data-testid')) {
                    shouldProcess = true;
                }
            });

            if (shouldProcess) {
                // Process immediately for new content
                setTimeout(throttledProcessAssets, 50);
            }
        }, 50));

        state.observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style', 'class', 'data-testid']
        });
    }

    // Enhanced scroll optimization with forced reprocessing
    let scrollTimeout;
    let lastScrollProcessTime = 0;
    
    function handleScroll() {
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(() => {
            const now = Date.now();
            const scrollPosition = window.innerHeight + window.scrollY;
            const documentHeight = document.documentElement.scrollHeight;
            
            // Always process when scrolling, but with different priorities
            if (scrollPosition >= documentHeight - 1500) {
                // Near bottom - high priority processing
                throttledProcessAssets();
                lastScrollProcessTime = now;
            } else if (now - lastScrollProcessTime > 2000) {
                // Periodic processing while scrolling
                throttledProcessAssets();
                lastScrollProcessTime = now;
            }
        }, 100);
    }

    // Additional intersection observer for better new content detection
    function setupIntersectionObserver() {
        if (!window.IntersectionObserver) return;
        
        const intersectionObserver = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting && !state.processedElements.has(entry.target)) {
                    // New element came into view, process it
                    setTimeout(throttledProcessAssets, 100);
                }
            });
        }, {
            root: null,
            rootMargin: '100px',
            threshold: 0.1
        });

        // Observe all existing cards
        const observeExistingCards = () => {
            const cards = document.querySelectorAll('div.fabkit-Stack-root.nTa5u2sc');
            cards.forEach(card => {
                if (!state.processedElements.has(card)) {
                    intersectionObserver.observe(card);
                }
            });
        };

        // Initial observation
        observeExistingCards();
        
        // Re-observe periodically to catch new elements
        setInterval(observeExistingCards, 3000);
    }

    // Create toggle button with better styling and state indication
    function createToggleButton() {
        const button = document.createElement('button');
        button.id = 'fab-hide-toggle';
        button.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 10000;
            background: #007bff;
            color: white;
            border: none;
            padding: 12px 16px;
            border-radius: 8px;
            cursor: pointer;
            font-size: 13px;
            font-weight: 600;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            transition: all 0.2s ease;
            user-select: none;
            min-width: 140px;
        `;

        function updateButtonState() {
            const hiddenCount = state.hiddenElements.size;
            if (state.isHidingEnabled) {
                button.textContent = `Show Saved (${hiddenCount})`;
                button.style.background = '#dc3545';
            } else {
                button.textContent = `Hide Saved (${hiddenCount})`;
                button.style.background = '#28a745';
            }
        }

        button.addEventListener('click', () => {
            toggleSavedAssets();
            updateButtonState();
        });

        button.addEventListener('mouseenter', () => {
            button.style.transform = 'translateY(-2px)';
            button.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.2)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.transform = 'translateY(0)';
            button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
        });

        updateButtonState();
        document.body.appendChild(button);

        // Update button count periodically
        setInterval(updateButtonState, 2000);
    }

    // Initialize the script
    function initialize() {
        console.log('Fab.com Hide Saved Assets script initialized');
        
        // Initial processing
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                setTimeout(() => {
                    throttledProcessAssets();
                    setupObserver();
                    setupIntersectionObserver();
                }, 500);
            });
        } else {
            setTimeout(() => {
                throttledProcessAssets();
                setupObserver();
                setupIntersectionObserver();
            }, 500);
        }

        // Add scroll listener for optimization
        window.addEventListener('scroll', handleScroll, { passive: true });
        
        // Create toggle button
        setTimeout(createToggleButton, 1000);

        // More aggressive periodic reprocessing
        setInterval(() => {
            if (!state.isProcessing) {
                throttledProcessAssets();
            }
        }, 3000);
        
        // Force full reprocessing periodically to catch missed elements
        setInterval(() => {
            console.log('Force reprocessing all elements...');
            state.processedElements = new WeakSet(); // Reset processed elements
            throttledProcessAssets();
        }, 5000);
    }

    // Cleanup on page unload
    window.addEventListener('beforeunload', () => {
        if (state.observer) {
            state.observer.disconnect();
        }
        window.removeEventListener('scroll', handleScroll);
    });

    // Start the script
    initialize();

})();