FlowX

The ultimate X experience: Seamless Audio, Liquid Glass, Perfect Layouts, and Custom Branding.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         FlowX
// @namespace    https://greasyfork.org/users/ghosty
// @version      2.1.1
// @description  The ultimate X experience: Seamless Audio, Liquid Glass, Perfect Layouts, and Custom Branding.
// @author       ghosty
// @match        https://x.com/*
// @match        https://www.x.com/*
// @match        https://twitter.com/*
// @match        https://www.twitter.com/*
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// @license      MIT
// @icon         https://abs.twimg.com/icons/apple-touch-icon-192x192.png
// ==/UserScript==

(function() {
    'use strict';

    // --- Config & State ---
    const CONFIG = {
        storageKeyVol: 'flowx_volume',
        storageKeyTheme: 'flowx_theme',
        storageKeyFont: 'flowx_font',
        storageKeyPos: 'flowx_position',
        storageKeyWide: 'flowx_widescreen',
        storageKeyDl: 'flowx_download',

        adKeywords: /^(Ad|Promoted|Sponsored|Sponsrad|Gesponsert|Publicité|Promocionado)$/i,
    };

    let state = {
        vol: parseFloat(localStorage.getItem(CONFIG.storageKeyVol)) || 0.5,
        theme: localStorage.getItem(CONFIG.storageKeyTheme) || 'liquid',
        font: localStorage.getItem(CONFIG.storageKeyFont) || 'default',
        wide: localStorage.getItem(CONFIG.storageKeyWide) === 'true',
        download: true, // Hardcoded ON to guarantee local storage doesn't hide it
        pos: JSON.parse(localStorage.getItem(CONFIG.storageKeyPos)) || { x: 20, y: window.innerHeight - 80 }
    };

    // --- THEME & TYPOGRAPHY ENGINE ---
    const STYLES = `
        :root {
            --spring-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
            --smooth-flow: cubic-bezier(0.25, 0.8, 0.25, 1);
            --glide-ease: cubic-bezier(0.22, 1, 0.36, 1);
            --fx-accent: #fff;
        }

        body.fx-font-cyber div[dir="auto"], body.fx-font-cyber span { font-family: "Courier New", Consolas, monospace !important; letter-spacing: -0.5px; }
        body.fx-font-serif div[dir="auto"], body.fx-font-serif span { font-family: Georgia, "Times New Roman", serif !important; }
        body.fx-font-rounded div[dir="auto"], body.fx-font-rounded span { font-family: ui-rounded, "Nunito", "Quicksand", sans-serif !important; }

        /* --- TRUE WIDESCREEN / FOCUS MODE --- */
        body.fx-mode-wide div[data-testid="sidebarColumn"] { display: none !important; width: 0 !important; }

        body.fx-mode-wide header[role="banner"] {
            position: absolute !important;
            left: 0; top: 0;
            transform: translateX(-95%) !important;
            opacity: 0 !important;
            transition: transform 0.3s var(--glide-ease), opacity 0.3s ease !important;
            z-index: 9999 !important;
            background: rgba(0,0,0,0.95) !important;
            height: 100vh !important;
            border-right: 1px solid rgba(255,255,255,0.1);
        }

        body.fx-mode-wide header[role="banner"]:hover {
            transform: translateX(0) !important;
            opacity: 1 !important;
        }

        body.fx-mode-wide header[role="banner"]::after {
            content: ''; position: absolute; top: 0; right: -25px; width: 25px; height: 100%;
        }

        body.fx-mode-wide main[role="main"],
        body.fx-mode-wide main[role="main"] > div,
        body.fx-mode-wide main[role="main"] > div > div {
            display: flex !important;
            justify-content: center !important;
            width: 100% !important;
            max-width: 100% !important;
        }

        body.fx-mode-wide div[data-testid="primaryColumn"] {
            margin: 0 auto !important;
            max-width: 700px !important;
            width: 100% !important;
            border: none !important;
        }

        body.fx-mode-wide div:has(> a[href="/i/grok"]),
        body.fx-mode-wide div:has(> div[data-testid="DMDrawer"]),
        body.fx-mode-wide [data-testid="DMDrawer"],
        body.fx-mode-wide a[href="/i/grok"] {
            display: none !important;
            opacity: 0 !important;
            pointer-events: none !important;
        }

        /* --- UI ELEMENTS --- */
        #flowx-btn {
            position: fixed; width: 42px; height: 42px; border-radius: 14px;
            display: flex; align-items: center; justify-content: center;
            font-weight: 800; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;
            cursor: grab; user-select: none;
            transition: transform 0.2s var(--smooth-flow), box-shadow 0.3s;
            box-shadow: 0 4px 12px rgba(0,0,0,0.4);
            z-index: 2147483647 !important;
        }
        #flowx-btn:active { cursor: grabbing; transform: scale(0.92); }
        #flowx-btn.fx-glide { transition: left 0.5s var(--glide-ease), top 0.5s var(--glide-ease), transform 0.3s; }
        #flowx-btn:hover { transform: scale(1.05) !important; }

        #flowx-panel {
            position: fixed; width: 340px; border-radius: 20px; padding: 20px;
            font-family: "Segoe UI", system-ui, sans-serif; display: none;
            max-height: 85vh; overflow-y: auto; overflow-x: hidden;
            z-index: 2147483647 !important;
        }

        .fx-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
        .fx-txt { font-size: 14px; font-weight: 500; opacity: 0.9; }

        .fx-toggle { position: relative; width: 44px; height: 24px; border-radius: 99px; background: rgba(120,120,120,0.3); cursor: pointer; transition: 0.3s; border: 1px solid rgba(255,255,255,0.1); flex-shrink: 0;}
        .fx-toggle::after { content: ''; position: absolute; left: 2px; top: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: transform 0.3s var(--spring-bounce); box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
        .fx-toggle.active { background: #34C759; }
        .fx-toggle.active::after { transform: translateX(20px); }

        .fx-slider-wrap { position: relative; width: 100%; height: 16px; display: flex; align-items: center; margin: 10px 0 24px 0; }
        .fx-slider-bg { position: absolute; left: 0; right: 0; height: 4px; border-radius: 99px; background: rgba(255,255,255,0.2); }
        .fx-slider-fill { position: absolute; left: 0; height: 4px; border-radius: 99px; pointer-events: none; }
        .fx-slider { -webkit-appearance: none; width: 100%; height: 20px; background: transparent; position: absolute; margin: 0; outline: none; cursor: pointer; }
        .fx-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.3); transition: transform 0.1s; margin-top: -7px; }
        .fx-slider::-webkit-slider-runnable-track { height: 4px; background: transparent; }

        .fx-select { width: 48%; padding: 10px; border-radius: 10px; font-size: 13px; outline: none; border:none; margin-bottom: 20px; cursor: pointer; background: rgba(0,0,0,0.2); color: inherit; font-weight: 600; }
        .fx-select-full { width: 100%; padding: 10px; border-radius: 10px; font-size: 13px; outline: none; border:none; margin-bottom: 20px; cursor: pointer; background: rgba(0,0,0,0.2); color: inherit; font-weight: 600; }
        .fx-select-wrap { display: flex; justify-content: space-between; }

        .fx-anim-open { animation: panelOpen 0.4s var(--spring-bounce) forwards; }
        .fx-anim-min { animation: panelMin 0.3s var(--smooth-flow) forwards; pointer-events: none; }
        @keyframes panelOpen { 0% { opacity: 0; transform: scale(0.8); filter: blur(10px); } 100% { opacity: 1; transform: scale(1); filter: blur(0px); } }
        @keyframes panelMin { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(0.6); filter: blur(10px); } }

        .fx-theme-liquid { background: rgba(20, 25, 35, 0.9); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 20px 50px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.3); color: #fff; }
        .fx-theme-liquid .fx-slider-fill { background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%); }

        .fx-theme-rgb { background: rgba(0, 0, 0, 0.9); border: 1px solid #333; color: #fff; animation: rgbBorder 4s linear infinite; }
        @keyframes rgbBorder { 0% { box-shadow: 0 0 10px #f00; border-color: #f00; } 50% { box-shadow: 0 0 10px #0f0; border-color: #0f0; } 100% { box-shadow: 0 0 10px #00f; border-color: #00f; } }
        .fx-theme-rgb .fx-slider-fill { background: linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red); }

        .fx-theme-cyber { background: rgba(5, 10, 16, 0.95); border: 1px solid #0ff; color: #0ff; box-shadow: 0 0 15px rgba(0,255,255,0.2); }
        .fx-theme-cyber .fx-slider-fill, .fx-theme-cyber .fx-toggle.active { background: #0ff; box-shadow: 0 0 10px #0ff; }
        .fx-theme-cyber .fx-toggle.active::after { background: #000; }

        .fx-theme-sunset { background: linear-gradient(135deg, rgba(255, 95, 109, 0.95), rgba(255, 195, 113, 0.95)); color: #fff; border: 1px solid rgba(255,255,255,0.4); }
        .fx-theme-sunset .fx-slider-fill { background: #fff; }

        .fx-theme-frost { background: rgba(255,255,255,0.85); backdrop-filter: blur(20px); color: #222; border: 1px solid rgba(0,0,0,0.1); }
        .fx-theme-frost .fx-slider-fill { background: #007AFF; }
        .fx-theme-frost .fx-toggle { background: rgba(0,0,0,0.1); }

        /* --- CUSTOM BRANDING ENGINE --- */
        .fx-logo-text {
            font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
            font-size: 22px !important;
            font-weight: 700 !important;
            color: #fff !important;
            margin-right: 10px !important;
            margin-left: 0 !important;
            vertical-align: middle !important;
            display: inline-block !important;
            letter-spacing: -0.5px !important;
            line-height: 1 !important;
            transition: opacity 0.3s ease;
        }

        /* Adjust the X SVG to align perfectly next to the text */
        div[role="banner"] h1[role="heading"] a[href="/home"] svg {
            display: inline-block !important;
            margin-left: 0 !important;
            position: relative;
            top: 1px;
        }

        body.fx-mode-wide header[role="banner"] .fx-logo-text {
            opacity: 0;
            pointer-events: none;
        }

        body.fx-mode-wide header[role="banner"]:hover .fx-logo-text {
            opacity: 1;
            pointer-events: auto;
        }

        /* --- THEMED KO-FI DONATION BUTTON --- */
        .fx-donate-wrapper {
            display: flex;
            justify-content: center;
            margin-top: 25px;
            padding-top: 15px;
            border-top: 1px solid rgba(255, 255, 255, 0.08);
        }

        .fx-donate-btn {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 8px 18px;
            background: rgba(255, 255, 255, 0.05);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 99px;
            color: #ffffff !important;
            text-decoration: none;
            font-weight: 700;
            font-size: 11px;
            letter-spacing: 0.5px;
            transition: all 0.3s var(--smooth-flow);
            text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
        }

        .fx-donate-btn span {
            font-size: 14px;
            transition: transform 0.4s var(--spring-bounce);
        }

        .fx-donate-btn:hover {
            transform: translateY(-2px) scale(1.02);
            color: #ffffff !important;
        }

        .fx-donate-btn:hover span {
            transform: scale(1.2) rotate(15deg);
        }

        .fx-donate-btn:active {
            transform: translateY(0) scale(0.95);
        }

        .fx-theme-liquid .fx-donate-btn:hover {
            background: rgba(0, 242, 254, 0.15);
            border-color: rgba(0, 242, 254, 0.5);
            box-shadow: 0 6px 15px rgba(0, 242, 254, 0.3);
        }
        .fx-theme-rgb .fx-donate-btn:hover {
            background: rgba(255, 255, 255, 0.1);
            border-color: #fff;
            animation: rgbBorder 4s linear infinite;
        }
        .fx-theme-cyber .fx-donate-btn:hover {
            background: rgba(0, 255, 255, 0.1);
            border-color: #0ff;
            color: #0ff !important;
            box-shadow: 0 0 15px rgba(0, 255, 255, 0.4);
        }
        .fx-theme-sunset .fx-donate-btn {
            border-color: rgba(255, 255, 255, 0.2);
            color: #ffffff !important;
        }
        .fx-theme-sunset .fx-donate-btn:hover {
            background: rgba(255, 255, 255, 0.2);
            border-color: #fff;
            box-shadow: 0 6px 15px rgba(255, 255, 255, 0.3);
        }
        .fx-theme-frost .fx-donate-wrapper {
            border-top: 1px solid rgba(0, 0, 0, 0.1);
        }
        .fx-theme-frost .fx-donate-btn {
            background: rgba(0, 0, 0, 0.05);
            border-color: rgba(0, 0, 0, 0.1);
            color: #333 !important;
            text-shadow: none;
        }
        .fx-theme-frost .fx-donate-btn:hover {
            background: #007AFF;
            border-color: #007AFF;
            color: #fff !important;
            box-shadow: 0 6px 15px rgba(0, 122, 255, 0.3);
        }
    `;

    const styleSheet = document.createElement("style");
    styleSheet.innerText = STYLES;
    document.head.appendChild(styleSheet);


    // --- THE AUDIO ENGINE ---
    document.addEventListener('pause', (e) => {
        if (e.target && e.target.tagName === 'VIDEO') {
            const rect = e.target.getBoundingClientRect();
            const screenCenter = window.innerHeight / 2;
            const vidCenterY = rect.top + (rect.height / 2);
            if (Math.abs(screenCenter - vidCenterY) < (window.innerHeight * 0.4)) {
                e.target.dataset.fxUserPaused = 'true';
            }
        }
    }, true);

    document.addEventListener('play', (e) => {
        if (e.target && e.target.tagName === 'VIDEO') {
            e.target.dataset.fxUserPaused = 'false';
        }
    }, true);

    const syncAudio = () => {
        const videos = document.querySelectorAll('video');
        if (videos.length === 0) return;

        const viewportHeight = window.innerHeight;
        const screenCenter = viewportHeight / 2;

        let closestVideo = null;
        let minDistance = Infinity;

        videos.forEach(v => {
            const rect = v.getBoundingClientRect();
            if (rect.height < 50) return;

            const vidCenterY = rect.top + (rect.height / 2);
            const distance = Math.abs(screenCenter - vidCenterY);
            if (distance < minDistance) {
                minDistance = distance;
                closestVideo = v;
            }
        });

        videos.forEach(v => {
            if (v === closestVideo && minDistance < (viewportHeight * 0.45)) {

                if (v.dataset.fxUserPaused === 'true') return;

                if (v.paused) {
                    v.muted = true;

                    const playPromise = v.play();
                    if (playPromise !== undefined) {
                        playPromise.then(() => {
                            v.muted = false;
                            v.volume = state.vol;
                        }).catch(() => {
                            const container = v.closest('[data-testid="videoComponent"]');
                            if (container) {
                                const playBtn = container.querySelector('[data-testid="playButton"]');
                                if (playBtn) playBtn.click();
                            }
                        });
                    }
                } else {
                    if (v.muted) v.muted = false;
                    if (Math.abs(v.volume - state.vol) > 0.05) v.volume = state.vol;
                }

            } else {
                if (!v.paused) v.pause();
            }
        });
    };

    let scrollTimeout;
    window.addEventListener('scroll', () => {
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(syncAudio, 100);
    }, { passive: true });
    setInterval(syncAudio, 600);


    // --- THE TARGETED, IMMUNE DOWNLOAD ENGINE ---
    function getTweetUrl(btn) {
        const article = btn.closest('article');
        if (!article) {
            if (window.location.pathname.includes('/status/')) return window.location.href;
            return null;
        }
        const links = Array.from(article.querySelectorAll('a[href*="/status/"]'));
        const timeLink = links.find(a => /\/status\/[0-9]+$/.test(a.href) && !a.href.includes('/photo/') && !a.href.includes('/video/'));
        return timeLink ? timeLink.href : null;
    }

    function injectDownloadButtons() {
        if (!state.download) return;

        // Hunt down the Bookmark button directly. We completely ignore the Action Bar [role="group"] because X moved it!
        const bookmarks = document.querySelectorAll('[data-testid="bookmark"], [data-testid="removeBookmark"]');

        bookmarks.forEach(bookmark => {
            try {
                // Get the physical button circle
                const btn = bookmark.closest('[role="button"]') || bookmark.closest('button') || bookmark;

                // Get the wrapper that physically separates it from the Share button
                const wrapper = btn.parentElement;
                if (!wrapper) return;

                // Get the master container that holds both Bookmark and Share
                const container = wrapper.parentElement;
                if (!container) return;

                // Check purely based on sibling DOM elements. If our button is to the left, stop.
                if (wrapper.previousElementSibling && wrapper.previousElementSibling.classList.contains('fx-dl-btn-raw')) return;

                // Build a pure HTML button from scratch.
                // We steal the exact layout CSS from the wrapper so X handles the Flexbox math perfectly.
                const dlBtn = document.createElement('div');
                dlBtn.className = wrapper.className + ' fx-dl-btn-raw';
                dlBtn.setAttribute('title', 'Download High-Res');

                // Hardcode X's native 34.75px circle geometry to absolutely prevent flexbox squashing
                dlBtn.style.cssText = 'display: flex; align-items: center; justify-content: center; width: 34.75px; height: 34.75px; border-radius: 9999px; cursor: pointer; color: rgb(113, 118, 123); transition: background-color 0.2s, color 0.2s;';

                // Pure vector SVG arrow matching X's 1.25em size spec
                dlBtn.innerHTML = '<svg viewBox="0 0 24 24" style="width: 1.25em; height: 1.25em; fill: currentColor; pointer-events: none;"><path d="M12 18.5l-6.5-6.5 1.414-1.414 4.086 4.086V3h2v11.672l4.086-4.086 1.414 1.414L12 18.5zm7 3v-2H5v2h14z"></path></svg>';

                // Native hover effects driven entirely by JS so React can't strip them
                dlBtn.addEventListener('mouseenter', () => {
                    dlBtn.style.backgroundColor = 'rgba(29, 155, 240, 0.1)';
                    dlBtn.style.color = 'rgb(29, 155, 240)';
                });
                dlBtn.addEventListener('mouseleave', () => {
                    dlBtn.style.backgroundColor = 'transparent';
                    dlBtn.style.color = 'rgb(113, 118, 123)';
                });

                // Execution Logic
                dlBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    const url = getTweetUrl(dlBtn);
                    if (!url) {
                        alert("FlowX: Couldn't extract the Tweet URL. Try clicking the tweet to expand it first.");
                        return;
                    }

                    // Green Success Pulse
                    dlBtn.style.color = '#34C759';
                    dlBtn.style.transform = 'scale(1.15)';
                    setTimeout(() => {
                        dlBtn.style.color = 'rgb(113, 118, 123)';
                        dlBtn.style.transform = 'scale(1)';
                    }, 800);

                    const apiUrl = url.replace(/(twitter\.com|x\.com)/, 'api.vxtwitter.com');

                    GM_xmlhttpRequest({
                        method: "GET",
                        url: apiUrl,
                        onload: function(response) {
                            try {
                                const data = JSON.parse(response.responseText);
                                if (data && data.mediaURLs && data.mediaURLs.length > 0) {
                                    data.mediaURLs.forEach((mediaUrl, index) => {
                                        let finalUrl = mediaUrl;

                                        const ext = finalUrl.split('.').pop().split('?')[0] || 'mp4';
                                        const filename = `FlowX_${data.tweetID}_${index}.${ext}`;

                                        GM_download({
                                            url: finalUrl,
                                            name: filename,
                                            onload: () => console.log('FlowX: Payload Secured ->', filename),
                                            onerror: () => window.open(finalUrl, '_blank')
                                        });
                                    });
                                } else {
                                    alert("FlowX: No media detected in this tweet.");
                                }
                            } catch(err) {
                                window.open(`https://x-downloader.net/?url=${encodeURIComponent(url)}`, '_blank');
                            }
                        },
                        onerror: function() {
                            window.open(`https://x-downloader.net/?url=${encodeURIComponent(url)}`, '_blank');
                        }
                    });
                });

                // Drop it perfectly inside the container, precisely to the left of the Bookmark button!
                container.insertBefore(dlBtn, wrapper);

            } catch (err) {
                // Catch any rendering oddities silently
            }
        });
    }

    // Heavy interval polling guarantees the button stays alive during fast scrolling
    setInterval(injectDownloadButtons, 500);
    const downloadObserver = new MutationObserver(() => injectDownloadButtons());
    downloadObserver.observe(document.body, { childList: true, subtree: true });


    // --- LOGO ENGINE ---
    function injectLogoText() {
        const logoAnchor = document.querySelector('h1[role="heading"] a[href="/home"], div[role="banner"] a[href="/home"]');

        if (!logoAnchor || logoAnchor.querySelector('.fx-logo-text')) return;

        const flowText = document.createElement('span');
        flowText.className = 'fx-logo-text';
        flowText.textContent = 'Flow';

        logoAnchor.style.display = 'flex';
        logoAnchor.style.alignItems = 'center';
        logoAnchor.style.flexDirection = 'row';

        logoAnchor.prepend(flowText);
    }
    setInterval(injectLogoText, 2000);


    // --- AD BLOCKER ---
    const processedNodes = new WeakSet();
    function processArticle(article, wrapper) {
        if (processedNodes.has(article)) return;
        processedNodes.add(article);

        const spans = article.querySelectorAll('span');
        let isAd = false;
        for (const span of spans) {
            const txt = span.textContent.trim();
            if (CONFIG.adKeywords.test(txt) && span.closest('[data-testid="User-Name"]')) {
                isAd = true; break;
            }
        }
        if (isAd) {
            if (wrapper) wrapper.style.display = 'none';
            else article.style.display = 'none';
            return;
        }
    }

    const observer = new MutationObserver((mutations) => {
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    if (node.matches && node.matches('div[data-testid="cellInnerDiv"]')) {
                        const article = node.querySelector('article[data-testid="tweet"]');
                        if (article) processArticle(article, node);
                    } else if (node.querySelectorAll) {
                        const wrappers = node.querySelectorAll('div[data-testid="cellInnerDiv"]');
                        wrappers.forEach(w => {
                            const a = w.querySelector('article[data-testid="tweet"]');
                            if (a) processArticle(a, w);
                        });
                    }
                }
            });
        });
    });
    observer.observe(document.body, { childList: true, subtree: true });


    // --- UI CONSTRUCTION ---
    function applyModes() {
        const b = document.body.classList;
        state.wide ? b.add('fx-mode-wide') : b.remove('fx-mode-wide');
        b.remove('fx-font-default', 'fx-font-cyber', 'fx-font-serif', 'fx-font-rounded');
        b.add(`fx-font-${state.font}`);
    }

    function createUI() {
        const btn = document.createElement('div');
        btn.id = 'flowx-btn';
        btn.textContent = 'FX';
        btn.className = `fx-theme-${state.theme} fx-glide`;
        btn.style.left = state.pos.x + 'px';
        btn.style.top = state.pos.y + 'px';

        const panel = document.createElement('div');
        panel.id = 'flowx-panel';
        panel.className = `fx-theme-${state.theme}`;

        panel.innerHTML = `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
                <div style="font-weight:700; font-size:18px;">FlowX <span style="font-size:11px; opacity:0.6; font-weight:400;">v2.1.1</span></div>
                <div class="fx-close" style="cursor:pointer; font-size:12px; opacity:0.7;">✕</div>
            </div>

            <div class="fx-select-wrap">
                <select class="fx-select" id="theme-select">
                    <option value="liquid">💧 Liquid</option>
                    <option value="rgb">🌈 RGB</option>
                    <option value="frost">❄️ Frost</option>
                    <option value="cyber">⚡ Cyber</option>
                    <option value="sunset">🌅 Sunset</option>
                </select>

                <select class="fx-select" id="font-select">
                    <option value="default">Aa System</option>
                    <option value="cyber">Aa Mono</option>
                    <option value="serif">Aa Serif</option>
                    <option value="rounded">Aa Smooth</option>
                </select>
            </div>

            <div style="margin-bottom:20px;">
                <div class="fx-row" style="margin-bottom:5px;">
                    <span class="fx-txt">Volume</span>
                    <span class="fx-vol-val" style="font-size:12px; font-weight:bold;">${Math.round(state.vol * 100)}%</span>
                </div>
                <div class="fx-slider-wrap">
                    <div class="fx-slider-bg"></div>
                    <div class="fx-slider-fill" style="width:${state.vol * 100}%"></div>
                    <input class="fx-slider" type="range" min="0" max="100" value="${state.vol * 100}">
                </div>
            </div>

            <div class="fx-txt" style="opacity:0.5; font-size:11px; margin-bottom:10px; text-transform:uppercase; letter-spacing:1px;">Features</div>

            <div id="feat-list"></div>

            <div class="fx-donate-wrapper">
                <a href="https://ko-fi.com/ghostyy69" target="_blank" class="fx-donate-btn">
                    <span>☕</span> support ghosty
                </a>
            </div>
        `;

        const features = [
            { id: 'wide', label: 'Widescreen', desc: 'Focus Mode' },
            { id: 'download', label: 'Download Button', desc: '1-Click API Rip' }
        ];

        const featList = panel.querySelector('#feat-list');
        features.forEach(f => {
            const row = document.createElement('div');
            row.className = 'fx-row';
            row.innerHTML = `
                <div>
                    <div class="fx-txt">${f.label}</div>
                    <div style="font-size:10px; opacity:0.5;">${f.desc}</div>
                </div>
                <div class="fx-toggle ${state[f.id] ? 'active' : ''}" id="tog-${f.id}"></div>
            `;
            featList.appendChild(row);

            row.querySelector('.fx-toggle').addEventListener('click', (e) => {
                state[f.id] = !state[f.id];
                e.currentTarget.classList.toggle('active');

                const map = {
                    'wide': CONFIG.storageKeyWide, 'download': CONFIG.storageKeyDl
                };
                localStorage.setItem(map[f.id], state[f.id]);

                applyModes();
            });
        });

        const slider = panel.querySelector('.fx-slider');
        const sliderFill = panel.querySelector('.fx-slider-fill');
        const volVal = panel.querySelector('.fx-vol-val');

        slider.addEventListener('input', (e) => {
            state.vol = e.target.value / 100;
            localStorage.setItem(CONFIG.storageKeyVol, state.vol);
            sliderFill.style.width = `${state.vol * 100}%`;
            volVal.innerText = `${Math.round(state.vol * 100)}%`;
        });

        const themeSelect = panel.querySelector('#theme-select');
        themeSelect.value = state.theme;
        themeSelect.addEventListener('change', (e) => {
            state.theme = e.target.value;
            localStorage.setItem(CONFIG.storageKeyTheme, state.theme);
            panel.className = `fx-theme-${state.theme} fx-anim-open`;
            btn.className = `fx-theme-${state.theme} fx-glide`;
        });

        const fontSelect = panel.querySelector('#font-select');
        fontSelect.value = state.font;
        fontSelect.addEventListener('change', (e) => {
            state.font = e.target.value;
            localStorage.setItem(CONFIG.storageKeyFont, state.font);
            applyModes();
        });

        panel.querySelector('.fx-close').addEventListener('click', () => togglePanel(false));

        applyModes();

        let isDragging = false, hasMoved = false, startX, startY, initX, initY;
        btn.addEventListener('mousedown', (e) => {
            isDragging = true; hasMoved = false;
            btn.classList.remove('fx-glide');
            startX = e.clientX; startY = e.clientY;
            initX = btn.offsetLeft; initY = btn.offsetTop;
        });

        document.documentElement.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true;
            btn.style.left = `${initX + dx}px`; btn.style.top = `${initY + dy}px`;
        }, true);

        document.documentElement.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                if (hasMoved) {
                    btn.classList.add('fx-glide');
                    const rect = btn.getBoundingClientRect();
                    const winW = window.innerWidth;
                    const snapX = (rect.left + rect.width/2 < winW/2) ? 20 : winW - rect.width - 20;
                    let snapY = rect.top;
                    if(snapY < 20) snapY = 20;
                    if(snapY > window.innerHeight - 80) snapY = window.innerHeight - 80;

                    btn.style.left = snapX + 'px'; btn.style.top = snapY + 'px';
                    state.pos = { x: snapX, y: snapY };
                    localStorage.setItem(CONFIG.storageKeyPos, JSON.stringify(state.pos));
                }
            }
        }, true);

        btn.addEventListener('click', () => {
            if (!hasMoved) togglePanel(panel.style.display !== 'block');
        });

        function togglePanel(show) {
            if (show) {
                panel.style.display = 'block';
                panel.classList.remove('fx-anim-min');
                panel.classList.add('fx-anim-open');

                const bRect = btn.getBoundingClientRect();

                let left = bRect.left + 60;
                if (left + 360 > window.innerWidth) left = bRect.left - 360;
                panel.style.left = Math.max(10, left) + 'px';

                if (bRect.top > window.innerHeight / 2) {
                    panel.style.top = 'auto';
                    panel.style.bottom = Math.max(20, window.innerHeight - bRect.bottom) + 'px';
                } else {
                    panel.style.bottom = 'auto';
                    panel.style.top = Math.max(20, bRect.top) + 'px';
                }

            } else {
                panel.classList.remove('fx-anim-open');
                panel.classList.add('fx-anim-min');
                setTimeout(() => { panel.style.display = 'none'; }, 280);
            }
        }

        document.body.appendChild(btn);
        document.body.appendChild(panel);
    }

    createUI();

})();