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.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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