Useful Twitch Toggles

Attempt at making shortcuts for Twitch. "w" is set to make viewable screen bigger, and "p" is set to toggle chat popped out into seperate window.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Useful Twitch Toggles
// @description  Attempt at making shortcuts for Twitch. "w" is set to make viewable screen bigger, and "p" is set to toggle chat popped out into seperate window.
// @version      6
// @namespace    twitch.popoutchat.and.fullscreen
// @author       Snorlaxing
// @match        http*://*.twitch.tv/*
// @match        http*://twitch.tv/*
// @match        http*://*twitch.tv
// @match        http*://*.twitch.tv/*
// @exclude      https://www.twitch.tv/popout/*/chat?popout=
// @exclude      https://www.twitch.tv/popout/*/chat?popout=
// @icon         https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png
// @grant        none
// ==/UserScript==


function wait(time) {
    return new Promise(resolve => {
        setTimeout(resolve, time);
    });
}

async function fixMarginTopAfterLoad(time, el, amt) {
    await wait(time);
    el.style.marginTop = amt;

}

(function() {
    'use strict';

    let currentPage = location.href;

    // listen for changes
    setInterval(function()
                {
        if (currentPage != location.href)
        {
            // page has changed, set new page as 'current'
            currentPage = location.href;
            console.log('Url has changed, reloading script');
            setup();

            if (poppedOut) {
                //attempt to change popOut to new chat
                var regexp = /(https?:\/\/www\.twitch\.tv\/)(\w+)\w*/g;
                var url = window.location.href;
                var result = [...url.matchAll(regexp)];
                var newUrl = result[0][1] + 'popout/' + result[0][2] + '/chat?popout='
                chatWindow.document.location.href = newUrl;
            }
        }
    }, 500);

    //globals
    var $ = window.jQuery;
    var fullScreenToggle = 'w';
    var popOutToggle = 'p';
    var toggles = [popOutToggle, fullScreenToggle];
    var poppedOut = false;
    var theatreMode = false;
    var mainWindow = window;
    var chatWindow = null;
    var closeChatIfLeavingStreamingChannel = true;

    //elements for theatre mode
    var previousMaxHeight = null;
    var topNav = null;
    var leftSideNav = null;
    var expandChatButton = null;
    var videoDiv = null;
    var mainPlayer = null;
    var persistentPlayer = null;
    var rootInfo = null;
    var onStreamer = false;

    //tracking for keyboard inputs
    var newKeyPressed = false;
    var singleLetter = true;
    var keyTimer = null;

    /*
    * This function is called whenever a key press is recorded.
    */
    function catchKeys(e) {
        //call to determine whether you are actually typing or pressing toggle key.
        registerToggleKeyPress(e);
    }


    /*
    * This function fills in all the global variables and handles closing chat if move to home page.
    * Change closeChatIfLeavingStreamingChannel to false, if you do not want chat to auto close when
    * you move to twitch home page or similar.
    */
    function setup() {

        console.log('Running script setup');
        window.removeEventListener("keydown", catchKeys, true);
        if (window.location.pathname.split('/').length > 1 && document.querySelectorAll('.channel-root--watch').length > 0) {

            onStreamer = true;
            console.log('On twitch streamer page, continuing setup');
            previousMaxHeight = document.querySelector('.persistent-player').style.maxHeight;
            topNav = document.querySelector('[data-a-target="top-nav-container"]');
            leftSideNav = document.querySelector('[data-test-selector="side-nav"]');
            expandChatButton = document.querySelector('.toggle-visibility__right-column');
            videoDiv = document.querySelector('.video-player__container--resize-calc');
            mainPlayer = document.querySelector('div.channel-root__player');
            persistentPlayer = document.querySelector('.persistent-player');
            rootInfo = document.querySelector('.channel-root__info');
            //document.addEventListener("keydown", catchKeys, true);
            window.addEventListener("keydown", catchKeys, false);

        } else {
            console.log('On home or other twitch page, no need to setup');
            onStreamer = false;
            if (poppedOut && chatWindow != null && closeChatIfLeavingStreamingChannel) {
                chatWindow.close();
            }
        }
    }


    /*
    * This function determines whether a single keyboard key was pressed or whether
    * user was typing.
    */
    function registerToggleKeyPress(e) {
        if (e.target != null) {
            //tpying into chat
            if (e.target.getAttribute('data-a-target') == 'chat-input') {
                return;
            }

            //tpying into input field
            try {
                if (isInputOrText(e.target)) {
                    return;
                }
            } catch (ex) {
                console.log(ex);
            }
        }

        //Otherwise, record time between keys until typing stops.
        if (keyTimer != null) {
            clearInterval(keyTimer);
            singleLetter = false;
        }
        newKeyPressed = true;
        keyTimer = setInterval(function() {
            if (!newKeyPressed) {

                clearInterval(keyTimer);
                if (singleLetter && toggles.includes(e.key)) {
                    console.log('Registered toggle key ' + e.key + ' pressed');
                    handleToggles(e);
                }

                keyTimer = null;
                singleLetter = true;
            }
            newKeyPressed = false;
        }, 400);
    }

    /*
    * This function handles the individual toggles.
    */
    function handleToggles(e) {
        if (e.key === fullScreenToggle && onStreamer) {
            if (theatreMode) {
                console.log('exit theatre mode');
                exitTheatreMode();
            } else if (!theatreMode) {
                console.log('enter theatre mode');
                enterTheatreMode();
            }
        } else if (e.key === popOutToggle && onStreamer) {
            if (!poppedOut && chatWindow == null) {
                popOutChat();
            } else {
                showChatOnSide();
            }
        }
    }

    function isInputOrText(element) {
        var tagName = element.tagName.toLowerCase();
        if (tagName === 'textarea') return true;
        if (tagName === 'input') return true;
        if (element.getAttribute('type') != null) {
            var type = element.getAttribute('type').toLowerCase(),
            // if any of these input types is not supported by a browser, it will behave as input type text.
            inputTypes = ['text', 'password', 'number', 'email', 'tel', 'url', 'search', 'date', 'datetime', 'datetime-local', 'time', 'month', 'week']
            return inputTypes.indexOf(type) >= 0;
        }
        return false;
    }

    setup();


    function exitTheatreMode() {

        topNav.removeAttribute("style");
        topNav.style.height = '5rem';
        leftSideNav.removeAttribute("style");
        expandChatButton.removeAttribute("style");
        expandChatButton.classList.add("kLMGYG");
        mainPlayer.classList.add('channel-root__player');
        mainPlayer.removeAttribute("style");
        videoDiv.removeAttribute("style");
        theatreMode = false;

        //Fixplayer
        persistentPlayer.style.top = '0px'
        persistentPlayer.style.left = '0px'
        persistentPlayer.style.maxHeight = previousMaxHeight;
        persistentPlayer.style.position = 'absolute';
        persistentPlayer.style.overflow = 'hidden';
        persistentPlayer.style.zIndex = '1';
        persistentPlayer.style.height = 'auto';
        persistentPlayer.style.transition = 'transform 0.5s ease 0s';
        persistentPlayer.style.transformOrigin = 'center top';
        persistentPlayer.style.transform = 'scale(1)';
    }

    function enterTheatreMode() {

        //If chat window is showing, close
        var right = document.querySelector('div.channel-root__right-column');
        if (right.classList.contains('channel-root__right-column--expanded')) {
            document.querySelector('[aria-label="Collapse Chat"]').click();
        }

        topNav.style.display = "none ";
        leftSideNav.style.display = "none";
        expandChatButton.style.display = 'none';
        expandChatButton.classList.remove("kLMGYG");
        mainPlayer.classList.remove('channel-root__player');
        mainPlayer.style.height = 'calc(100vh)';
        persistentPlayer.style.maxHeight = 'calc(100vh)';
        videoDiv.style.maxHeight = '100%';
        fixMarginTopAfterLoad(500, rootInfo, '0px');
        persistentPlayer.style.display = 'flex';
        persistentPlayer.style.height = '100%';
        persistentPlayer.style.flexDirection = 'column';
        persistentPlayer.style.justifyContent = 'center';
        persistentPlayer.style.backgroundColor = 'black';
        theatreMode = true;
    }

    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

            // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    function popOutChat() {

        //Get right expand element
        var right = document.querySelector('div.channel-root__right-column');

        //Get site details for popup
        var regexp = /(https?:\/\/www\.twitch\.tv\/)(\w+)\w*/g;
        var url = window.location.href;
        var result = [...url.matchAll(regexp)];
        var newUrl = result[0][1] + 'popout/' + result[0][2] + '/chat?popout='
        chatWindow = window.open(newUrl,'chat','toolbar=0,status=0,height=500,width=400');
        poppedOut = true;

        //Close side panel if open
        if (right.classList.contains('channel-root__right-column--expanded')) {
            document.querySelector('[aria-label="Collapse Chat"]').click();
        }

        console.log('Chat Popped Out');
        //Track new window
        trackNewWindow();
    }

    function showChatOnSide() {
        //if already poppedOut
        if (poppedOut && chatWindow != null) {
            //close chatWindow
            chatWindow.close();
        }

        //Open right Side
        var right = document.querySelector('div.channel-root__right-column');
        if (!right.classList.contains('channel-root__right-column--expanded')) {
            document.querySelector('[aria-label="Expand Chat"]').click();
        }

        console.log('Chat closed and opened in side panel');

    }

    function trackNewWindow() {
        var timer = setInterval(function() {
            if(chatWindow.closed) {
                clearInterval(timer);
                poppedOut = false;
                chatWindow = null;
            }
        }, 1000);
    }
})();