XenForo Forum FAB Menu (Bottom Right)

Adds a floating action button (FAB) at the bottom right to access the account menu/alerts and notifications without scrolling back to the top. Supports JAV-Forum, SimpCity, and SocialMediaGirls.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         XenForo Forum FAB Menu (Bottom Right)
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Adds a floating action button (FAB) at the bottom right to access the account menu/alerts and notifications without scrolling back to the top. Supports JAV-Forum, SimpCity, and SocialMediaGirls.
// @author       Gemini 3 Pro
// @match        https://jav-forum.com/*
// @match        https://www.jav-forum.com/*
// @match        https://simpcity.su/*
// @match        https://simpcity.li/*
// @match        https://simpcity.cr/*
// @match        https://forums.socialmediagirls.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ================= CONFIGURATION & SELECTORS =================
    const HOST = window.location.hostname;
    const FADE_DELAY = 2000;
    const FADED_OPACITY = '0.3';

    let SITE_CONFIG = {
        trigger: '',      // Element to click/read badge from
        avatarSrc: '',    // Element to copy visual style from
        badgeAttr: 'data-badge'
    };

    // Detect Site and apply selectors
    if (HOST.includes('jav-forum')) {
        // JAV-Forum: The user link contains both the avatar and the menu trigger
        SITE_CONFIG.trigger = '.p-navgroup-link--user';
        SITE_CONFIG.avatarSrc = '.p-navgroup-link--user img'; 
    } else if (HOST.includes('simpcity') || HOST.includes('socialmediagirls')) {
        // SimpCity & SMG: 
        // Trigger = The Alerts/Bell icon (as requested for notifications)
        // Avatar = The User icon (for visual appearance)
        SITE_CONFIG.trigger = 'a[href*="/account/alerts"]'; 
        SITE_CONFIG.avatarSrc = '.p-navgroup-link--user .avatar'; 
    }
    // =============================================================

    let fabButton = null;
    let triggerElement = null; // The actual element we click/read badge
    let avatarSourceElement = null; // The element we copy the image/look from
    let fadeTimer = null;

    // 1. Inject CSS Styles
    const css = `
        /* FAB Container Styles */
        #jf-fab-container {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9990;
            cursor: pointer;
            transition: transform 0.2s, opacity 0.5s ease-in-out;
            -webkit-tap-highlight-color: transparent;
            opacity: 1;
        }

        #jf-fab-container.jf-fab-faded {
            opacity: ${FADED_OPACITY};
        }

        #jf-fab-container:hover,
        #jf-fab-container:active {
            opacity: 1 !important;
        }

        #jf-fab-container:active {
            transform: scale(0.95);
        }

        /* Avatar Circle Styles */
        .jf-fab-avatar {
            width: 56px;
            height: 56px;
            border-radius: 50%;
            overflow: hidden;
            box-shadow: 0 4px 10px rgba(0,0,0,0.3);
            border: 2px solid #fff;
            background-color: #eee;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        /* Support for Image Avatars */
        .jf-fab-avatar img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        /* Support for XenForo Dynamic/Text Avatars (SimpCity/SMG) */
        .jf-fab-avatar .avatar {
            width: 100% !important;
            height: 100% !important;
            font-size: 28px !important; /* Adjust font size for the FAB */
            line-height: 56px !important;
            border-radius: 0 !important; /* Container is already rounded */
            margin: 0 !important;
        }

        /* Red Notification Badge */
        .jf-fab-badge {
            position: absolute;
            top: -5px;
            right: -5px;
            background-color: #E53935;
            color: white;
            border-radius: 10px;
            padding: 2px 6px;
            font-size: 11px;
            font-weight: bold;
            border: 2px solid #fff;
            min-width: 18px;
            text-align: center;
            display: none;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        .jf-fab-badge.has-unread {
            display: block;
        }

        /* Force Menu Position for all sites */
        body.fab-mode .menu--structural {
            position: fixed !important;
            top: auto !important;
            bottom: 90px !important;
            right: 20px !important;
            left: auto !important;
            transform: none !important;
            max-height: 80vh;
            overflow-y: auto;
            z-index: 9999 !important;
            transform-origin: bottom right !important;
            animation: fabMenuFadeIn 0.2s ease-out;
        }

        @keyframes fabMenuFadeIn {
            from { opacity: 0; transform: translateY(10px) scale(0.95); }
            to { opacity: 1; transform: translateY(0) scale(1); }
        }
    `;
    GM_addStyle(css);

    // 2. Initialize
    function init() {
        // Find the main trigger (for badge and click)
        triggerElement = document.querySelector(SITE_CONFIG.trigger);
        
        // Find the avatar visual source. If distinct selector not found, fallback to trigger
        avatarSourceElement = document.querySelector(SITE_CONFIG.avatarSrc) || triggerElement;

        if (!triggerElement) {
            console.log("FAB Menu: Not logged in or selectors changed.");
            return;
        }

        createFab();
        syncData();
        setupObservers();
        scheduleFade();
    }

    function scheduleFade() {
        if (fadeTimer) clearTimeout(fadeTimer);
        fadeTimer = setTimeout(() => {
            if (fabButton && !document.body.classList.contains('fab-mode')) {
                fabButton.classList.add('jf-fab-faded');
            }
        }, FADE_DELAY);
    }

    function wakeUp() {
        if (fadeTimer) clearTimeout(fadeTimer);
        if (fabButton) fabButton.classList.remove('jf-fab-faded');
    }

    // 3. Create FAB
    function createFab() {
        const container = document.createElement('div');
        container.id = 'jf-fab-container';

        const avatarDiv = document.createElement('div');
        avatarDiv.className = 'jf-fab-avatar';

        // Logic to clone the avatar (Image or Dynamic Span)
        updateAvatarVisuals(avatarDiv);

        const badgeSpan = document.createElement('span');
        badgeSpan.className = 'jf-fab-badge';
        badgeSpan.innerText = '0';

        container.appendChild(avatarDiv);
        container.appendChild(badgeSpan);
        document.body.appendChild(container);

        fabButton = container;
        fabButton.addEventListener('click', handleFabClick);
        fabButton.addEventListener('mouseenter', wakeUp);
        fabButton.addEventListener('mouseleave', scheduleFade);
    }

    // Helper to copy/clone avatar into FAB
    function updateAvatarVisuals(containerDiv) {
        containerDiv.innerHTML = ''; // Clear current

        if (!avatarSourceElement) return;

        // check if it's an IMG tag
        if (avatarSourceElement.tagName === 'IMG') {
            const img = document.createElement('img');
            img.src = avatarSourceElement.src;
            containerDiv.appendChild(img);
        } 
        // check if it's a dynamic SPAN (SimpCity/SMG) or IMG wrapped in specific class
        else {
            // Clone the node to keep background colors/text
            const clone = avatarSourceElement.cloneNode(true);
            // Remove ID to avoid conflicts
            clone.removeAttribute('id');
            containerDiv.appendChild(clone);
        }
    }

    // 4. Handle FAB Click
    function handleFabClick(e) {
        e.preventDefault();
        e.stopPropagation();
        wakeUp();

        const isMenuOpen = triggerElement.classList.contains('is-menuOpen');

        if (isMenuOpen) {
            // Close logic
            document.body.classList.remove('fab-mode');
            // Trigger a click on body or the trigger to close XF menu
            triggerElement.click(); 
            scheduleFade(); 
        } else {
            // Open logic
            document.body.classList.add('fab-mode');
            triggerElement.click();

            setTimeout(() => {
                const closer = () => {
                    document.body.classList.remove('fab-mode');
                    scheduleFade();
                };
                // Listen for the next click anywhere to close (XenForo handles the actual close)
                document.addEventListener('click', closer, { once: true });
            }, 50);
        }
    }

    // 5. Sync Data
    function syncData() {
        if (!fabButton || !triggerElement) return;

        // 1. Sync Badge Count
        const count = triggerElement.getAttribute(SITE_CONFIG.badgeAttr);
        const badgeEl = fabButton.querySelector('.jf-fab-badge');

        if (count && parseInt(count) > 0) {
            badgeEl.innerText = count;
            badgeEl.classList.add('has-unread');
            wakeUp();
            scheduleFade();
        } else {
            badgeEl.classList.remove('has-unread');
        }

        // 2. Sync Avatar (Optional, if user changes pfp via ajax, rare but possible)
        // Only re-render if we suspect a change (checking src or content)
        // For simplicity in this lightweight script, we assume avatar doesn't change rapidly without page reload.
    }

    // 6. Setup Observers
    function setupObservers() {
        // Observe the Trigger for Badge changes
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'attributes' || mutation.type === 'childList') {
                    syncData();
                }
            });
        });

        observer.observe(triggerElement, {
            attributes: true,
            childList: true,
            subtree: true,
            attributeFilter: [SITE_CONFIG.badgeAttr, 'class']
        });
    }

    init();

})();