Grok TidyFavs

Hides tagged images in the "All" section of your saved imagine creations.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Grok TidyFavs
// @namespace    https://github.com/Zhiro90
// @version      1.3.2
// @description  Hides tagged images in the "All" section of your saved imagine creations. 
// @author       Zhiro90
// @match        *://grok.com/*
// @icon         https://grok.com/images/favicon.ico
// @homepageURL  https://github.com/Zhiro90/grok-tidyfavs
// @supportURL   https://github.com/Zhiro90/grok-tidyfavs/issues
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 🎛️ CONFIGURACIÓN DE DESARROLLO
    // ==========================================
    const DEBUG_MODE = false;

    let savedMemory = JSON.parse(localStorage.getItem('grok_tagged_memory') || '{}');
    let hideTagged = localStorage.getItem('grok_hide_tagged') !== 'false';

    let currentFolderState = undefined;
    let zoomTimeout = null;
    let isScrolling = false;
    let scrollTimeout = null;
    let pendingLayoutFix = false;

    function logDebug(type, message, color = "#bdc3c7") {
        if (!DEBUG_MODE) return;
        console.log(`%c[DEBUG ${type}] ${message}`, `color: ${color}; font-size: 11px;`);
    }

    console.log("%c🚀 GROK TIDYFAVS V1.3.2 (INVISIBLE VOLUME HACK) LOADED", "color: #00ff00; font-weight: bold;");

    // ==========================================
    // 🎨 CSS INJECTIONS (Global Scroll Lock)
    // ==========================================
    if (!document.getElementById('grok-tidy-css')) {
        const style = document.createElement('style');
        style.id = 'grok-tidy-css';
        style.textContent = `
            body.tidy-scroll-lock {
                overflow: hidden !important;
                overscroll-behavior: none !important;
            }
        `;
        document.head.appendChild(style);
    }

    // ==========================================
    // 🕹️ 1. SCROLL GUARD
    // ==========================================
    window.addEventListener('scroll', () => {
        isScrolling = true;
        clearTimeout(scrollTimeout);

        scrollTimeout = setTimeout(() => {
            isScrolling = false;
            if (pendingLayoutFix) {
                logDebug("SCROLL", "Scroll stopped. Triggering sync.", "#2ecc71");
                pendingLayoutFix = false;
                requestLayoutSync();
            }
        }, 500);
    }, { capture: true, passive: true });

    // ==========================================
    // 🖥️ 2. DOM MANAGER & HACKS
    // ==========================================
    const SYSTEM_TAB_NAMES = new Set([
        'All', 'Todas', 'Todos', 'Tout', 'Tous', 'Alle', 'Tutti', 'すべて', '全て', '全部',
        'Favorites', 'Favourites', 'Guardados', 'Favoritos', 'Favoris', 'Favoriten', 'Preferiti', 'Saved',
    ]);

    function getMediaId(node) {
        if (node.dataset.grokMediaId) return node.dataset.grokMediaId;
        try {
            const key = Object.keys(node).find(k => k.startsWith('__reactFiber$'));
            if (!key) return null;
            let curr = node[key];
            for (let i = 0; i < 25; i++) {
                if (!curr) break;
                const props = curr.memoizedProps;
                const data = props?.data || props?.post || props?.item;
                if (data?.id && typeof data.id === 'string' && data.id.length > 20) {
                    node.dataset.grokMediaId = data.id;
                    return data.id;
                }
                curr = curr.return;
            }
        } catch (e) {}
        return null;
    }

    function getActiveFolderName() {
        const activeBtn = document.querySelector('button.bg-primary');
        if (!activeBtn) return null;
        const name = activeBtn.textContent.trim();
        return SYSTEM_TAB_NAMES.has(name) ? null : (name || null);
    }

    function showToast(message, type = 'info') {
        const existingToasts = document.querySelectorAll('.grok-tidyfavs-toast');
        existingToasts.forEach(t => t.remove());

        const toast = document.createElement('div');
        toast.className = 'grok-tidyfavs-toast';
        toast.textContent = message;

        let bgColor = 'rgba(0, 0, 0, 0.9)';
        let borderColor = '#444';

        if (type === 'success') {
            bgColor = 'rgba(20, 80, 40, 0.95)';
            borderColor = '#2ecc71';
        } else if (type === 'warning') {
            bgColor = 'rgba(100, 30, 30, 0.95)';
            borderColor = '#ff4d4d';
        }

        Object.assign(toast.style, {
            position: 'fixed', bottom: '85px', right: '20px', zIndex: '9999999',
            backgroundColor: bgColor, color: '#fff',
            padding: '12px 18px', borderRadius: '8px', border: `1px solid ${borderColor}`,
            fontSize: '14px', fontFamily: 'sans-serif', fontWeight: '500',
            boxShadow: '0 4px 12px rgba(0,0,0,0.5)', pointerEvents: 'none',
            transition: 'opacity 0.3s ease-in-out', opacity: '0'
        });
        document.body.appendChild(toast);
        requestAnimationFrame(() => { toast.style.opacity = '1'; });
        setTimeout(() => { toast.style.opacity = '0'; }, 2000);
        setTimeout(() => { toast.remove(); }, 2300);
    }

    function triggerZoom() {
        const originalZoom = document.body.style.zoom || '';
        document.body.style.zoom = '0.999';
        setTimeout(() => {
            document.body.style.zoom = originalZoom;
            window.dispatchEvent(new Event('resize'));
        }, 50);
    }

    function requestLayoutSync() {
        if (isScrolling) {
            pendingLayoutFix = true;
            return;
        }
        if (zoomTimeout) clearTimeout(zoomTimeout);
        // Grok recomendó subir esto a 300ms para darle respiro al Virtualizer
        zoomTimeout = setTimeout(triggerZoom, 300);
    }

    function fireTripleTapSequence() {
        setTimeout(triggerZoom, 100);
        setTimeout(triggerZoom, 500);
        setTimeout(triggerZoom, 1200);
    }

    // 🔥 EL HACK DE VOLUMEN INVISIBLE RECOMENDADO POR GROK
    function applyShrinkHide(wrapper) {
        if (wrapper.dataset.tidyShrink === 'true') return false;

        wrapper.style.removeProperty('display');

        // El sweet spot: 10px de altura para calmar a React, pero 100% invisible.
        wrapper.style.setProperty('height', '10px', 'important');
        wrapper.style.setProperty('min-height', '10px', 'important');
        wrapper.style.setProperty('width', '1px', 'important');
        wrapper.style.setProperty('overflow', 'hidden', 'important');
        wrapper.style.setProperty('opacity', '0', 'important');
        wrapper.style.setProperty('margin', '0', 'important');
        wrapper.style.setProperty('padding', '0', 'important');
        wrapper.style.setProperty('pointer-events', 'none', 'important');
        wrapper.style.setProperty('border', 'none', 'important');

        wrapper.dataset.tidyShrink = 'true';
        return true;
    }

    function removeShrinkHide(wrapper) {
        if (wrapper.dataset.tidyShrink !== 'true') return false;

        wrapper.style.removeProperty('height');
        wrapper.style.removeProperty('min-height');
        wrapper.style.removeProperty('width');
        wrapper.style.removeProperty('overflow');
        wrapper.style.removeProperty('opacity');
        wrapper.style.removeProperty('margin');
        wrapper.style.removeProperty('padding');
        wrapper.style.removeProperty('pointer-events');
        wrapper.style.removeProperty('border');

        delete wrapper.dataset.tidyShrink;
        return true;
    }

    // ==========================================
    // 👁️ THE MUTATION OBSERVER (Zero-Blink Engine)
    // ==========================================
    const domObserver = new MutationObserver((mutations) => {
        const url = window.location.href;
        if (!url.includes('favorites') && !url.includes('collection') && !url.includes('saved') && !url.endsWith('/all')) return;

        let needsSync = false;
        const isAllView = !getActiveFolderName();

        for (const mutation of mutations) {
            if (mutation.type === 'childList') {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1) {
                        const cards = node.classList.contains('group/media-post-masonry-card')
                            ? [node]
                            : node.querySelectorAll('.group\\/media-post-masonry-card');

                        cards.forEach(card => {
                            const wrapper = card.parentElement;
                            const mediaId = getMediaId(card);

                            if (mediaId && hideTagged && isAllView && savedMemory[mediaId]) {
                                applyShrinkHide(wrapper);
                                needsSync = true;
                            }
                        });
                    }
                }
            }
        }

        if (needsSync) {
            logDebug("OBSERVER", "Squished hidden elements instantly before paint.");
            if (window.scrollY === 0 && isAllView) {
                window.scrollTo(0, 2);
            }
        }
    });

    domObserver.observe(document.body, { childList: true, subtree: true });

    // ==========================================
    // 🧠 MEMORY MANAGER (Background Sync)
    // ==========================================
    function updateDOM() {
        const folderName = getActiveFolderName();
        const cards = Array.from(document.querySelectorAll('.group\\/media-post-masonry-card'));
        if (cards.length === 0) return;

        let learnedCount = 0;
        let maxRelativeBottom = 0;
        let needsLayoutFix = false;

        const isAllView = !folderName;
        const masonryContainer = cards[0].parentElement.parentElement;
        const containerRect = masonryContainer ? masonryContainer.getBoundingClientRect() : null;

        const newFolderState = isAllView ? 'ALL' : folderName;
        if (currentFolderState !== newFolderState) {
            currentFolderState = newFolderState;
            if (isAllView && hideTagged) {
                fireTripleTapSequence();
            }
        }

        cards.forEach(card => {
            const wrapper = card.parentElement;
            const mediaId = getMediaId(card);

            if (!mediaId) return;

            if (!isAllView) {
                if (removeShrinkHide(wrapper)) needsLayoutFix = true;
                if (!savedMemory[mediaId]) {
                    savedMemory[mediaId] = folderName;
                    learnedCount++;
                }
            } else if (hideTagged && savedMemory[mediaId]) {
                if (applyShrinkHide(wrapper)) needsLayoutFix = true;
            } else {
                if (removeShrinkHide(wrapper)) needsLayoutFix = true;

                if (hideTagged && isAllView && containerRect && wrapper.dataset.tidyShrink !== 'true') {
                    const reactTransformY = parseFloat(wrapper.style.translate?.split(' ')[1] || "0");
                    const relativeBottom = reactTransformY + wrapper.getBoundingClientRect().height;
                    if (relativeBottom > maxRelativeBottom) {
                        maxRelativeBottom = relativeBottom;
                    }
                }
            }
        });

        if (needsLayoutFix) {
            logDebug("RENDER", "Applying Zoom Jiggle to force React measurer update");
            requestLayoutSync();
        }

        let currentTargetHeight = "100vh";

        if (isAllView && hideTagged && masonryContainer) {
            if (maxRelativeBottom > 0) {
                let proposedHeight = Math.ceil(maxRelativeBottom) + 800;
                let currentHeightVal = parseFloat(masonryContainer.dataset.tidyHeight || "0");

                if (Math.abs(proposedHeight - currentHeightVal) > 15) {
                    currentTargetHeight = `${proposedHeight}`;
                    masonryContainer.dataset.tidyHeight = currentTargetHeight;
                    masonryContainer.style.setProperty('height', `${currentTargetHeight}px`, 'important');
                    masonryContainer.dataset.tidyBrake = 'true';
                }
            } else {
                masonryContainer.style.setProperty('height', '100vh', 'important');
                masonryContainer.dataset.tidyHeight = "0";
            }
        } else if (masonryContainer && masonryContainer.dataset.tidyBrake) {
            masonryContainer.style.removeProperty('height');
            delete masonryContainer.dataset.tidyBrake;
            delete masonryContainer.dataset.tidyHeight;
        }

        if (learnedCount > 0) {
            localStorage.setItem('grok_tagged_memory', JSON.stringify(savedMemory));
            showToast(`✅ ${learnedCount} new images added to memory`, 'success');
        }
    }

    // ==========================================
    // 🎛️ 3. USER INTERFACE
    // ==========================================
    function createUI() {
        if (document.getElementById('grok-filter-container')) return;

        const containerUI = document.createElement('div');
        containerUI.id = 'grok-filter-container';
        Object.assign(containerUI.style, {
            position: 'fixed', bottom: '20px', right: '80px', zIndex: '999999',
            display: 'flex', flexDirection: 'column', gap: '10px', alignItems: 'center'
        });

        const resetBtn = document.createElement('button');
        resetBtn.title = "Clear memory (Already refreshed? Reload to show hidden)";
        Object.assign(resetBtn.style, {
            width: '38px', height: '38px', borderRadius: '50%',
            backgroundColor: 'rgba(20,20,20,0.9)', border: '1px solid #555',
            cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
            color: '#aaa', fontSize: '18px', boxShadow: '0 2px 6px rgba(0,0,0,0.4)', transition: 'all 0.2s'
        });
        resetBtn.innerHTML = `
            <svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="3 6 5 6 21 6"></polyline>
                <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
            </svg>
        `;
        resetBtn.onmouseover = () => { resetBtn.style.color = '#ff4d4d'; resetBtn.style.borderColor = '#ff4d4d'; };
        resetBtn.onmouseout = () => { resetBtn.style.color = '#aaa'; resetBtn.style.borderColor = '#555'; };
        resetBtn.onclick = () => {
            savedMemory = {};
            localStorage.setItem('grok_tagged_memory', '{}');
            hideTagged = false;
            localStorage.setItem('grok_hide_tagged', false);

            const tb = document.getElementById('grok-toggle-btn');
            if(tb) {
                tb.style.color = '#fff';
                tb.innerHTML = '👁️';
                tb.title = 'Hide organized images';
            }
            showToast("Memory wiped. (Reload if images are missing)", 'warning');
            fireTripleTapSequence();
        };

        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'grok-toggle-btn';
        Object.assign(toggleBtn.style, {
            width: '50px', height: '50px', borderRadius: '50%',
            backgroundColor: 'rgba(0,0,0,0.9)', border: '2px solid #444',
            cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
            color: hideTagged ? '#ff4d4d' : '#fff', fontSize: '24px', boxShadow: '0 4px 10px rgba(0,0,0,0.5)', transition: 'color 0.2s'
        });

        toggleBtn.innerHTML = hideTagged ? '👁️‍🗨️' : '👁️';
        toggleBtn.title = hideTagged ? 'Show organized images' : 'Hide organized images';

        toggleBtn.onclick = () => {
            hideTagged = !hideTagged;
            localStorage.setItem('grok_hide_tagged', hideTagged);
            toggleBtn.style.color = hideTagged ? '#ff4d4d' : '#fff';
            toggleBtn.innerHTML = hideTagged ? '👁️‍🗨️' : '👁️';
            toggleBtn.title = hideTagged ? 'Show organized images' : 'Hide organized images';
            showToast(hideTagged ? "Applying Filter..." : "Disabling Filter...", "info");
            fireTripleTapSequence();
        };

        containerUI.appendChild(resetBtn);
        containerUI.appendChild(toggleBtn);
        document.body.appendChild(containerUI);
    }

    // ==========================================
    // ⏰ MASTER LOOP
    // ==========================================
    setInterval(() => {
        const url = window.location.href;
        const isTargetPage = url.includes('favorites') || url.includes('collection') || url.includes('saved') || url.endsWith('/all');

        let uiContainer = document.getElementById('grok-filter-container');

        if (!isTargetPage) {
            if (uiContainer) uiContainer.style.display = 'none';
            document.body.classList.remove('tidy-scroll-lock');
            return;
        } else {
            if (uiContainer) {
                uiContainer.style.display = 'flex';
            } else {
                createUI();
            }
            document.body.classList.add('tidy-scroll-lock');
            updateDOM();
        }
    }, 150);
})();