Grok TidyFavs

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();