Stripchat Native Fullscreen - Smart Exit

Detects native or site fullscreen and hides the chat once immediately, while still allowing the user to show it again.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Stripchat Native Fullscreen - Smart Exit
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Detects native or site fullscreen and hides the chat once immediately, while still allowing the user to show it again.
// @author       Thiago
// @match        https://stripchat.com/*
// @match        https://*.stripchat.com/*
// @grant        none
// ==/UserScript==

// Changelog:
// v1.8 - Hides the chat once immediately after native or site fullscreen starts.
//        Supports auto-hidden player controls and lets the user show the chat again.
// v1.7 - Native fullscreen with manual-exit detection and the "T" shortcut.

(function () {
    'use strict';

    const CHAT_BUTTON_RETRY_MS = 200;
    const CHAT_BUTTON_MAX_WAIT_MS = 10000;
    const FULLSCREEN_STATE_POLL_MS = 250;

    const STATE = {
        userExited: false,
        autoTried: false,
        fullscreenActive: false,
        chatClickDone: false,
        chatHideTimer: null,
        chatSearchTimer: null
    };

    const isVisible = (element) => {
        if (!element) return false;
        const rect = element.getBoundingClientRect();
        const style = getComputedStyle(element);
        return rect.width > 0 &&
               rect.height > 0 &&
               style.display !== 'none' &&
               style.visibility !== 'hidden';
    };

    const getFSButton = () => {
        return document.querySelector('button .icon-fullscreen-on')?.closest('button') ||
               document.querySelector('button .icon-fullscreen-off')?.closest('button');
    };

    const isFullscreenActive = () => {
        const siteFullscreenButton = document.querySelector('button .icon-fullscreen-off')?.closest('button');
        return Boolean(
            document.fullscreenElement ||
            (siteFullscreenButton && isVisible(siteFullscreenButton))
        );
    };

    const toggleNativeFS = (force = false) => {
        const fsButton = getFSButton();
        const isCurrentlyFS = isFullscreenActive();

        if (fsButton && (force || (!isCurrentlyFS && !STATE.userExited))) {
            fsButton.click();
        }
    };

    const wakeFullscreenControls = () => {
        const fullscreenRoot = document.fullscreenElement ||
                               document.querySelector('.player-controls-fullscreen__main') ||
                               document.documentElement;

        const rect = fullscreenRoot.getBoundingClientRect();
        const clientX = Math.round(rect.left + rect.width / 2);
        const clientY = Math.round(rect.top + Math.min(rect.height * 0.25, 180));
        const targets = [
            fullscreenRoot.querySelector('video'),
            fullscreenRoot,
            document.body,
            document,
            window
        ].filter(Boolean);

        const mouseOptions = {
            bubbles: true,
            cancelable: true,
            composed: true,
            view: window,
            clientX,
            clientY,
            screenX: clientX,
            screenY: clientY,
            movementX: 1,
            movementY: 1
        };

        for (const target of targets) {
            try {
                if (typeof PointerEvent === 'function') {
                    target.dispatchEvent(new PointerEvent('pointermove', {
                        ...mouseOptions,
                        pointerId: 1,
                        pointerType: 'mouse',
                        isPrimary: true
                    }));
                }
                target.dispatchEvent(new MouseEvent('mousemove', mouseOptions));
                target.dispatchEvent(new MouseEvent('mouseover', mouseOptions));
                target.dispatchEvent(new MouseEvent('mouseenter', {
                    ...mouseOptions,
                    bubbles: false
                }));
            } catch (_) {}
        }

        console.log('[SC-FS] Fullscreen controls wake-up requested.');
    };

    const describeControl = (element) => {
        const icon = element.querySelector('[class], svg, i');
        return [
            element.getAttribute('title'),
            element.getAttribute('aria-label'),
            element.getAttribute('data-testid'),
            element.getAttribute('data-test'),
            element.className,
            icon?.getAttribute('class'),
            element.textContent
        ].filter(Boolean).join(' ').toLowerCase();
    };

    const getChatToggleButton = () => {
        const fullscreenRoot = document.fullscreenElement || document;

        // Known specific selectors take priority.
        const specificSelectors = [
            '.player-chat-button',
            '.fullscreen-chat-toggle',
            '.player-chat-toggle',
            '.chat-toggle',
            '.toggle-chat',
            '[class*="chat-toggle" i]',
            '[class*="chat_button" i]',
            '[data-testid*="chat-toggle" i]',
            'button:has(.icon-chat-off)',
            'button:has(.icon-chat-on)',
            'button:has([class*="chat-disabled" i])',
            'button:has([class*="chat-enabled" i])'
        ];

        for (const selector of specificSelectors) {
            const match = Array.from(fullscreenRoot.querySelectorAll(selector))
                .map((element) => element.closest('button, [role="button"]') || element)
                .find(isVisible);
            if (match) return match;
        }

        // Fallback that does not depend on a fixed Stripchat class name.
        const candidates = Array.from(
            fullscreenRoot.querySelectorAll('button, [role="button"]')
        ).filter(isVisible);

        return candidates.find((element) => {
            const description = describeControl(element);
            const controlsChatOrInterface = /chat|interface|overlay/.test(description);
            const hidesIt = /hide|disable|close|off|ocultar|desabilitar|desativar|fechar/.test(description);
            const unrelated = /resolution|quality|qualidade|volume|sound|mute|tip|fullscreen-(on|off)/.test(description);
            return controlsChatOrInterface && hidesIt && !unrelated;
        }) || null;
    };

    const chatIsAlreadyHidden = (button) => {
        const actionText = [
            button.getAttribute('title'),
            button.getAttribute('aria-label'),
            button.textContent
        ].filter(Boolean).join(' ').toLowerCase();

        // The label describes what clicking will do. "Hide Chat" therefore
        // means the chat is visible now and the button must be clicked.
        if (/hide|disable|close|ocultar|desabilitar|desativar|fechar/.test(actionText)) return false;
        if (/show|enable|open|mostrar|habilitar|ativar/.test(actionText)) return true;

        const iconState = [
            button.className,
            button.querySelector('[class], svg, i')?.getAttribute('class')
        ].filter(Boolean).join(' ').toLowerCase();

        // Stripchat uses icon-chat-enabled while the chat is visible and
        // icon-chat-disabled after it has been hidden.
        if (/chat-disabled|chat-off|chat-hidden/.test(iconState)) return true;
        if (/chat-enabled|chat-on|chat-active/.test(iconState)) return false;

        // Fullscreen opens with chat visible by default.
        return false;
    };

    const clearChatTimers = () => {
        if (STATE.chatHideTimer) {
            clearTimeout(STATE.chatHideTimer);
            STATE.chatHideTimer = null;
        }
        if (STATE.chatSearchTimer) {
            clearInterval(STATE.chatSearchTimer);
            STATE.chatSearchTimer = null;
        }
    };

    const clickChatToggleOnce = () => {
        if (STATE.chatClickDone || !isFullscreenActive()) return true;

        const button = getChatToggleButton();
        if (!button) return false;

        if (chatIsAlreadyHidden(button)) {
            STATE.chatClickDone = true;
            console.log('[SC-FS] The chat was already hidden; no click was needed.');
            return true;
        }

        // Set the flag before clicking to prevent repetition from synchronous events.
        STATE.chatClickDone = true;
        console.log('[SC-FS] Hiding the chat/interface with a single click.', button);
        button.click();
        return true;
    };

    const findAndClickChatToggle = () => {
        wakeFullscreenControls();
        return clickChatToggleOnce();
    };

    const hideChatOnFullscreen = () => {
        clearChatTimers();

        if (!isFullscreenActive() || STATE.chatClickDone) return;

        // The controls are normally still visible at this point, so try now.
        if (findAndClickChatToggle()) return;

        // If Stripchat has not mounted the button yet, retry lookup without
        // performing any extra click after the first successful one.
        const searchStartedAt = Date.now();
        STATE.chatSearchTimer = setInterval(() => {
            const timedOut = Date.now() - searchStartedAt >= CHAT_BUTTON_MAX_WAIT_MS;
            const clickedOrAlreadyHidden = findAndClickChatToggle();
            if (clickedOrAlreadyHidden || timedOut || !isFullscreenActive()) {
                clearInterval(STATE.chatSearchTimer);
                STATE.chatSearchTimer = null;
            }
        }, CHAT_BUTTON_RETRY_MS);
    };

    const syncFullscreenState = () => {
        const fullscreenActive = isFullscreenActive();
        if (fullscreenActive === STATE.fullscreenActive) return;

        STATE.fullscreenActive = fullscreenActive;

        if (fullscreenActive) {
            STATE.chatClickDone = false;
            console.log('[SC-FS] Fullscreen active; hiding the chat once now.');
            hideChatOnFullscreen();
        } else {
            STATE.userExited = true;
            STATE.chatClickDone = false;
            clearChatTimers();
            console.log('[SC-FS] Manual fullscreen exit detected and respected.');
        }
    };

    document.addEventListener('fullscreenchange', () => {
        // Stripchat may update its own fullscreen icon immediately after this event.
        setTimeout(syncFullscreenState, 0);
    });

    // Use the user's first interaction to request fullscreen.
    document.addEventListener('click', function once() {
        if (!STATE.autoTried) {
            toggleNativeFS();
            STATE.autoTried = true;
        }
    }, { capture: true, once: true });

    // Press "T" to toggle fullscreen manually.
    document.addEventListener('keydown', (event) => {
        const activeTag = document.activeElement?.tagName || '';
        if (event.key.toLowerCase() === 't' && !['INPUT', 'TEXTAREA'].includes(activeTag)) {
            STATE.userExited = false;
            toggleNativeFS(true);
        }
    });

    const observer = new MutationObserver(() => {
        if (!STATE.autoTried && getFSButton()) {
            toggleNativeFS();
            STATE.autoTried = true;
        }
        syncFullscreenState();
    });

    observer.observe(document.body, { childList: true, subtree: true });
    setInterval(syncFullscreenState, FULLSCREEN_STATE_POLL_MS);
    syncFullscreenState();
})();