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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();