Tabview Youtube

Make comments and lists into tabs

目前为 2021-07-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         Tabview Youtube
// @namespace    http://tampermonkey.net/
// @version      0.9.3
// @description  Make comments and lists into tabs
// @author       CY Fung
// @match        https://www.youtube.com/watch?v=*
// @resource     contentCSS https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube/bcfe64940c5656740bf3aab24265b05993c6c85c/css/style_content.css
// @icon         https://github.com/cyfung1031/Tabview-Youtube/raw/main/images/icon128p.png
// @require      https://code.jquery.com/jquery-3.6.0.slim.min.js
// @grant        GM_getResourceText
// @run-at       document-start
// @license      MIT https://github.com/cyfung1031/Tabview-Youtube/blob/main/LICENSE
// ==/UserScript==
function main($){
    // MIT License
    // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js























/**
 * SVG resources:
 * <div>Icons made by <a href="https://www.flaticon.com/authors/smashicons" title="Smashicons">Smashicons</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
 */

const scriptVersionForExternal = '2021/07/03';

 const svgComments = `
 <path d="M40.068,13.465L5.93,13.535c-3.27,0-5.93,2.66-5.93,5.93v21.141c0,3.27,2.66,5.929,5.93,5.929H12v10
 c0,0.413,0.254,0.784,0.64,0.933c0.117,0.045,0.239,0.067,0.36,0.067c0.276,0,0.547-0.115,0.74-0.327l9.704-10.675l16.626-0.068
 c3.27,0,5.93-2.66,5.93-5.929V19.395C46,16.125,43.34,13.465,40.068,13.465z M10,23.465h13c0.553,0,1,0.448,1,1s-0.447,1-1,1H10
 c-0.553,0-1-0.448-1-1S9.447,23.465,10,23.465z M36,37.465H10c-0.553,0-1-0.448-1-1s0.447-1,1-1h26c0.553,0,1,0.448,1,1
 S36.553,37.465,36,37.465z M36,31.465H10c-0.553,0-1-0.448-1-1s0.447-1,1-1h26c0.553,0,1,0.448,1,1S36.553,31.465,36,31.465z"/>
 <path d="M54.072,2.535L19.93,2.465c-3.27,0-5.93,2.66-5.93,5.93v3.124l26.064-0.054c4.377,0,7.936,3.557,7.936,7.93v21.07v0.071
 v2.087l3.26,3.586c0.193,0.212,0.464,0.327,0.74,0.327c0.121,0,0.243-0.022,0.36-0.067c0.386-0.149,0.64-0.52,0.64-0.933v-10h1.07
 c3.27,0,5.93-2.66,5.93-5.929V8.465C60,5.195,57.34,2.535,54.072,2.535z"/>
 `

const svgVideos = `<path d="M298,33c0-13.255-10.745-24-24-24H24C10.745,9,0,19.745,0,33v232c0,13.255,10.745,24,24,24h250c13.255,0,24-10.745,24-24V33
 z M91,39h43v34H91V39z M61,259H30v-34h31V259z M61,73H30V39h31V73z M134,259H91v-34h43V259z M123,176.708v-55.417
 c0-8.25,5.868-11.302,12.77-6.783l40.237,26.272c6.902,4.519,6.958,11.914,0.056,16.434l-40.321,26.277
 C128.84,188.011,123,184.958,123,176.708z M207,259h-43v-34h43V259z M207,73h-43V39h43V73z M268,259h-31v-34h31V259z M268,73h-31V39
 h31V73z"/>`

const svgInfo = `<path d="M11.812,0C5.289,0,0,5.289,0,11.812s5.289,11.813,11.812,11.813s11.813-5.29,11.813-11.813
 S18.335,0,11.812,0z M14.271,18.307c-0.608,0.24-1.092,0.422-1.455,0.548c-0.362,0.126-0.783,0.189-1.262,0.189
 c-0.736,0-1.309-0.18-1.717-0.539s-0.611-0.814-0.611-1.367c0-0.215,0.015-0.435,0.045-0.659c0.031-0.224,0.08-0.476,0.147-0.759
 l0.761-2.688c0.067-0.258,0.125-0.503,0.171-0.731c0.046-0.23,0.068-0.441,0.068-0.633c0-0.342-0.071-0.582-0.212-0.717
 c-0.143-0.135-0.412-0.201-0.813-0.201c-0.196,0-0.398,0.029-0.605,0.09c-0.205,0.063-0.383,0.12-0.529,0.176l0.201-0.828
 c0.498-0.203,0.975-0.377,1.43-0.521c0.455-0.146,0.885-0.218,1.29-0.218c0.731,0,1.295,0.178,1.692,0.53
 c0.395,0.353,0.594,0.812,0.594,1.376c0,0.117-0.014,0.323-0.041,0.617c-0.027,0.295-0.078,0.564-0.152,0.811l-0.757,2.68
 c-0.062,0.215-0.117,0.461-0.167,0.736c-0.049,0.275-0.073,0.485-0.073,0.626c0,0.356,0.079,0.599,0.239,0.728
 c0.158,0.129,0.435,0.194,0.827,0.194c0.185,0,0.392-0.033,0.626-0.097c0.232-0.064,0.4-0.121,0.506-0.17L14.271,18.307z
  M14.137,7.429c-0.353,0.328-0.778,0.492-1.275,0.492c-0.496,0-0.924-0.164-1.28-0.492c-0.354-0.328-0.533-0.727-0.533-1.193
 c0-0.465,0.18-0.865,0.533-1.196c0.356-0.332,0.784-0.497,1.28-0.497c0.497,0,0.923,0.165,1.275,0.497
 c0.353,0.331,0.53,0.731,0.53,1.196C14.667,6.703,14.49,7.101,14.137,7.429z"/>`

const svgPlayList = `
 <rect x="0" y="64" width="256" height="42.667"/>
 <rect x="0" y="149.333" width="256" height="42.667"/>
 <rect x="0" y="234.667" width="170.667" height="42.667"/>
 <polygon points="341.333,234.667 341.333,149.333 298.667,149.333 298.667,234.667 213.333,234.667 213.333,277.333 
     298.667,277.333 298.667,362.667 341.333,362.667 341.333,277.333 426.667,277.333 426.667,234.667"/>
     `

class WeakRefer{

    
    constructor(obj){

        if(obj){
            if(window.WeakRef) this._obj1= new WeakRef(obj);
            else this._obj2=obj;
        }

    }

    deref(){

        return (this._obj1?this._obj1.deref():this._obj2)||null

    }

}

function isNonEmptyString(s){
    return typeof s=='string'&&s.length>0;
}



const LAYOUT_TWO_COLUMNS=1;
const LAYOUT_THEATER=2;
const LAYOUT_FULLSCREEN=4;
const LAYOUT_CHATROOM=8;
const LAYOUT_CHATROOM_COLLASPED=16;
const LAYOUT_TAB_EXPANDED = 32;




function layoutStatusChanged(old_layoutStatus, new_layoutStatus){

    const cssElm=ytdFlexy.deref();

    if(!cssElm)return;

    let changes;

    if(old_layoutStatus===null){

        changes=LAYOUT_TWO_COLUMNS | LAYOUT_THEATER | LAYOUT_CHATROOM | LAYOUT_CHATROOM_COLLASPED | LAYOUT_TAB_EXPANDED;

    }else{
        changes = old_layoutStatus^new_layoutStatus
    }

    //console.log(1060,changes)

    const new_isExpandedChat = !(new_layoutStatus & LAYOUT_CHATROOM_COLLASPED)  && (new_layoutStatus & LAYOUT_CHATROOM)
    const new_isCollaspedChat = (new_layoutStatus & LAYOUT_CHATROOM_COLLASPED)  && (new_layoutStatus & LAYOUT_CHATROOM)

    const new_isTwoColumns = new_layoutStatus & LAYOUT_TWO_COLUMNS;
    const new_isTheater = new_layoutStatus & LAYOUT_THEATER;
    const new_isTabExpanded = new_layoutStatus & LAYOUT_TAB_EXPANDED;


    function showTabOrChat(){

        layoutStatusMutex.lockWith(unlock=>{

            if(lastShowTab=='#chatroom') {
                
                if(new_isTabExpanded) switchTabActivity(null)
                if( !new_isExpandedChat ) ytBtnExpandChat(); 
            
            }else{

                if( new_isExpandedChat ) ytBtnCollapseChat()
                if(!new_isTabExpanded) setToActiveTab();

            } 

            setTimeout(unlock,40);

        })
    }

    function hideTabAndChat(){

        layoutStatusMutex.lockWith(unlock=>{
            
            if( new_isTabExpanded) switchTabActivity(null)
            if( new_isExpandedChat ) ytBtnCollapseChat()


            setTimeout(unlock,40);

        })

    }

    //console.log(7001)

 
    if(  (changes & LAYOUT_CHATROOM_COLLASPED) && new_isExpandedChat  && !new_isTheater && new_isTwoColumns && new_isTabExpanded ){

        //console.log(7011)
        switchTabActivity(null);

    } else if( ((changes & LAYOUT_CHATROOM_COLLASPED)||(changes & LAYOUT_TAB_EXPANDED)) && new_isTwoColumns && !new_isTheater && new_isCollaspedChat && !new_isTabExpanded ){

        //console.log(7012)
        ytBtnSetTheater();

    }else if( (((changes & LAYOUT_CHATROOM_COLLASPED ) && new_isExpandedChat) ||((changes & LAYOUT_TAB_EXPANDED ) && new_isTabExpanded)) && !(changes & LAYOUT_THEATER) && !(changes & LAYOUT_TWO_COLUMNS) && new_isTheater && new_isTwoColumns ){

        //console.log(7013)
        ytBtnCancelTheater();

    
    }else if(  (changes & LAYOUT_THEATER || changes & LAYOUT_TWO_COLUMNS) && !new_isTheater && new_isTwoColumns  && !new_isExpandedChat && !new_isTabExpanded){ 
        
        //console.log(7014)
        showTabOrChat();

    }else if( (changes & LAYOUT_TWO_COLUMNS) && new_isTwoColumns && !new_isTheater && new_isExpandedChat && new_isTabExpanded ){

        //console.log(7015)
        showTabOrChat();


    }else if(  !(changes & LAYOUT_THEATER) && (changes & LAYOUT_TWO_COLUMNS) && new_isTwoColumns && new_isTheater  &&  (new_isTabExpanded || new_isExpandedChat)   ){

        //console.log(7016)

        hideTabAndChat()


    }else if(  new_isTwoColumns && new_isTheater  &&  (new_isTabExpanded || new_isExpandedChat)   ){

        //console.log(7017)
        hideTabAndChat()


    }else{
        
        //console.log(7018)
    }
    

    

}


const $ws={
    _layoutStatus:null,
    _cid:0
}
Object.defineProperty($ws, 'layoutStatus', {
    get(){
        return this._layoutStatus;
    },
    set(nv){

        if(nv===null){
            this._layoutStatus=null;
            return;
        }
        if(nv === this._layoutStatus) return;

        if(!this._cid) {
            this._cid=1;
            const old_layoutStatus=this._layoutStatus;
            
            layoutStatusMutex.lockWith(unlock=>{

                this._cid=0;
                const new_layoutStatus = this._layoutStatus;

                //console.log(1051, old_layoutStatus, new_layoutStatus)

                layoutStatusChanged(old_layoutStatus, new_layoutStatus);

                setTimeout(unlock,40)


            })
        }

        this._layoutStatus=nv;

    },
    enumerable:true,
    configurable:true
})



     const prettyElm = function(elm) {
        if (!elm || !elm.nodeName) return null;
        const eId = elm.id || null;
        const eClsName = elm.className || null;
        return [elm.nodeName.toLowerCase(), typeof eId == 'string' ? "#" + eId : '', typeof eClsName == 'string' ? '.' + eClsName.replace(/\s+/g, '.') : ''].join('').trim();
    }
    function addScript(scriptText) {
        const scriptNode = document.createElement('script');
        scriptNode.type = 'text/javascript';
        scriptNode.textContent = scriptText;
        document.documentElement.appendChild(scriptNode);
        return scriptNode;
    }



const svgElm = (w, h, vw, vh, p) => `<svg width="${w}" height="${h}" viewBox="0 0 ${vw} ${vh}" preserveAspectRatio="xMidYMid meet">${p}</svg>`

let settings = {
    toggleSettings: {
        tabs: 1,
        tInfo: 1,
        tComments: 1,
        tVideos: 1,
    },
    defaultTab: "videos"
};

const mtoInterval1=40;
const mtoInterval2=150;

const clickInterval1=100;
const clickInterval2=30;

let mtoInterval = mtoInterval1;
let clickInterval=clickInterval1;

function getAttribute(elm,key){
    return elm?elm.getAttribute(key):null
}

function isVideoPlaying(video) {
    return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > video.HAVE_CURRENT_DATA;
}

function setAttr(elm, attrName, b){

    if(!elm)return;
    if(b) elm.setAttribute(attrName,''); else elm.removeAttribute(attrName);
}

function hideTabBtn($tabBtn){
    
    var isActiveBefore = $tabBtn.is('.active')

    $tabBtn.addClass("tab-btn-hidden");
    if (isActiveBefore) {
        setToActiveTab();
    }
}

function hasAttribute(obj, key){
    return obj && obj.hasAttribute(key);
}

function isTheater(){
    
    const cssElm=ytdFlexy.deref();
    return (cssElm && cssElm.hasAttribute('theater'))
}

function isChatExpand(){
    const cssElm=ytdFlexy.deref();
    return cssElm && cssElm.hasAttribute('userscript-chatblock') && !cssElm.hasAttribute('userscript-chat-collapsed')
}
function isWideScreenWithTwoColumns(){
    const cssElm=ytdFlexy.deref();
    return (cssElm && cssElm.hasAttribute('is-two-columns_'))
    
}

function isAnyActiveTab(){
    return $('#right-tabs .tab-btn.active').length>0
}

function ytBtnSetTheater(){
    if(!isTheater()){
        const sizeBtn =  document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
        if(sizeBtn) sizeBtn.click();
    }
}
function ytBtnCancelTheater(){
    if(isTheater()){
        const sizeBtn =  document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
        if(sizeBtn) sizeBtn.click();
    }
}

function ytBtnExpandChat(){
    
    let button = document.querySelector('ytd-live-chat-frame#chat[collapsed]>.ytd-live-chat-frame#show-hide-button')
    if (button) button.querySelector('ytd-toggle-button-renderer').click();
}
function ytBtnCollapseChat(){
    
    let button = document.querySelector('ytd-live-chat-frame#chat:not([collapsed])>.ytd-live-chat-frame#show-hide-button')
    if (button) button.querySelector('ytd-toggle-button-renderer').click();
}


function hackImgShadow(imgShadow){
    // add to #columns and add back after loaded
    let img = imgShadow.querySelector('img')
    if(!img)return;

    let p=imgShadow.parentNode
    let z=$(imgShadow).clone()[0]; //to occupy the space
    p.replaceChild(z, imgShadow)
    $(imgShadow).prependTo('#columns'); // refer to css hack

    function onload(evt){
        if(evt) this.removeEventListener('load',onload,false)
        p.replaceChild(imgShadow, z)
        p=null;
        z=null;
        imgShadow=null;
    }

    if (img.complete) onload();
    else img.addEventListener('load',onload,false)
}


const Q={}

Q.$callOnceAsync=async function(key){
    if (Q[key] && Q[key]() === false) Q[key] = null
}

function chatFrameElement(cssSelector){
    let iframe = document.querySelector('iframe#chatframe');
    if(!iframe) return null;
    let cDoc = iframe.contentDocument;
    if(!cDoc) return null;
    if(cDoc.readyState  != 'complete') return null; //we must wait for its completion
    let elm = null;
    try{
        elm = cDoc.querySelector(cssSelector)
    }catch(e){
        console.log('iframe error', e)
    }
    return elm;
}


















function fixRelated(){
    
    if(!document.querySelector("#tab-videos>[placeholder-videos]>ytd-watch-next-secondary-results-renderer[data-dom-changed-by-tabview-youtube]")){



        let relatedVideos = document.querySelector("#related>ytd-watch-next-secondary-results-renderer");
        if(relatedVideos){

            $('[placeholder-videos]').removeAttr('placeholder-videos')
            $('[placeholder-for-youtube-play-next-queue]').removeAttr('placeholder-for-youtube-play-next-queue')

            let $parentNode= $(relatedVideos.parentNode).appendTo(document.querySelector("#tab-videos"))
            $(relatedVideos).attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)

            $parentNode.attr('placeholder-for-youtube-play-next-queue','').attr('placeholder-videos','')

            $('[placeholder-videos]').scroll(makeBodyScrollByEvt);

        }

        
    }
}

function extractTextContent(elm){
    return elm.textContent.replace(/\s+/g,'').replace(/[^\da-zA-Z\u4E00-\u9FFF\u00C0-\u00FF\u00C0-\u02AF\u1E00-\u1EFF\u0590-\u05FF\u0400-\u052F\u0E00-\u0E7F\u0600-\u06FF\u0750-\u077F\u1100-\u11FF\u3130-\u318F\uAC00-\uD7AF\u3040-\u30FF\u31F0-\u31FF]/g,'')
}

function mtf_fixAutoCompletePosition(elmAutoComplete){

var injectionScript_fixAutoComplete=function(){

    // https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.min.js

    for(const s of document.querySelectorAll('[autocomplete="off"]:not([data-autocomplete-results-id])')){


        let sc = s.sc;
        if(sc instanceof HTMLElement){

            let id=+new Date;
            s.setAttribute('data-autocomplete-results-id',id);
            sc.setAttribute('data-autocomplete-input-id', id);
            
            if(window.WeakRef){
                s._sc=new WeakRef(sc);
                s.sc=null;
                delete s.sc;
                Object.defineProperty(s,'sc',{
                    get: function() { return s._sc.deref()||null; },  
                    enumerable: true,
                    configurable: true
                })
            }

            if(sc.hasAttribute('autocomplete-disable-updatesc') && typeof s.updateSC =='function'){

                window.removeEventListener('resize', s.updateSC);
                s.updateSC=function(){};

            }

            sc.dispatchEvent(new CustomEvent('autocomplete-sc-exist'));


        }

    }

};

function handlerAutoCompleteExist(){


    elmAutoComplete.removeEventListener('autocomplete-sc-exist', handlerAutoCompleteExist, false)

    let autoComplete = this;
    let domId= autoComplete.getAttribute('data-autocomplete-input-id')
    let searchBox = autoComplete.ownerDocument.querySelector(`[data-autocomplete-results-id="${domId}"]`)


    if(!domId || !searchBox) return;

    let aaa=searchBox.nextSibling;
    if(aaa && aaa.nodeName.toLowerCase()=="autocomplete-positioner"){
    }else if(aaa && aaa.nodeName.toLowerCase()!="autocomplete-positioner"){
        $(aaa=document.createElement("autocomplete-positioner")).insertAfter(searchBox);
    }else{
        $(aaa=document.createElement("autocomplete-positioner")).prependTo(searchBox.parentNode);
    }
    $(autoComplete).prependTo(aaa);




    aaa.style.setProperty('--sb-margin-bottom',getComputedStyle(searchBox).marginBottom)
    aaa.style.setProperty('--height',searchBox.offsetHeight + 'px')


}

elmAutoComplete.setAttribute('autocomplete-disable-updatesc','')
elmAutoComplete.addEventListener('autocomplete-sc-exist',handlerAutoCompleteExist, false)

addScript(`!!(${injectionScript_fixAutoComplete+''})()`)
injectionScript_fixAutoComplete=null;



}

function mtf_fixTabsAtTheEnd(){
    // if window resize, youtube coding will relocate the element
    // for example, chatroom move before #right-tabs
    // causing difference apperance after resize of window


    fixRelated();
    

    let nonLastRightTabs = document.querySelector('#secondary #right-tabs:not(:last-child)')

    if(nonLastRightTabs){
        nonLastRightTabs.parentNode.appendChild(nonLastRightTabs)
    }

    let chatroom = document.querySelector('#primary ytd-live-chat-frame#chat');
    if(chatroom){
        let right_tabs = document.querySelector('#secondary #right-tabs:last-child')
        if(right_tabs){


            right_tabs.parentNode.insertBefore(chatroom, right_tabs)

        }
        
    }


    const autocomplete=document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search + autocomplete-positioner > .autocomplete-suggestions[data-autocomplete-input-id]:not([position-fixed-by-tabview-youtube])')

    if(autocomplete){

        const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')


        if(searchBox){

            
            autocomplete.parentNode.setAttribute('position-fixed-by-tabview-youtube','');
            autocomplete.setAttribute('position-fixed-by-tabview-youtube','');
            autocomplete.setAttribute('userscript-scrollbar-render','')

            if(!searchBox.hasAttribute('is-set-click-to-toggle')){
                searchBox.setAttribute('is-set-click-to-toggle','')
                searchBox.addEventListener('click',function(){

                    setTimeout(()=>{
                        const autocomplete=document.querySelector(`.autocomplete-suggestions[data-autocomplete-input-id="${ this.getAttribute('data-autocomplete-results-id') }"]`)

                        if(!autocomplete)return;

                        const isNotEmpty = (autocomplete.textContent||'').length>0 && (this.value||'').length>0;
                        
                        if( isNotEmpty ){

                            let elmVisible=$(autocomplete).is(':visible')

                            if(elmVisible) $(autocomplete).hide(); else  $(autocomplete).show();

                        }

                    },20);

                })

                let cid_searchbox_keyup=0;
                searchBox.addEventListener('keyup',function(){

                    if(cid_searchbox_keyup)return;
                    cid_searchbox_keyup=setTimeout(()=>{
                        cid_searchbox_keyup=0;
                        
                        const autocomplete=document.querySelector(`.autocomplete-suggestions[data-autocomplete-input-id="${ this.getAttribute('data-autocomplete-results-id') }"]`)

                        if(!autocomplete)return;
                        

                        const isNotEmpty = (autocomplete.textContent||'').length>0 && (this.value||'').length >0
                        
                        if( isNotEmpty ){

                            let elmVisible=$(autocomplete).is(':visible')

                            if(!elmVisible) $(autocomplete).show();

                        }

                    },20);

                })

            }



        }

    }





    
    let currentLastVideo=document.querySelector('[placeholder-videos] #items ytd-compact-video-renderer:last-of-type')
    let prevLastVideo=_cachedLastVideo?_cachedLastVideo.deref():null;
    _cachedLastVideo = new WeakRefer(currentLastVideo);
    
    if(prevLastVideo && currentLastVideo && prevLastVideo!==currentLastVideo){

        let isPrevRemoved= !prevLastVideo.parentNode


        function getVideoListHash(){

            let res = [...document.querySelectorAll('[placeholder-videos] #items ytd-compact-video-renderer')].map(renderer=>{
                return renderer.querySelector('a[href*="watch"][href*="v="]').getAttribute('href')

            }).join('|')
            // /watch?v=XXXXX|/watch?v=XXXXXX|/watch?v=XXXXXX

/*

            let elms = document.querySelectorAll('[placeholder-videos] #items ytd-compact-video-renderer')
            
            let res = [...elms].map(elm=>elm.data.videoId||'').join('|') ;
          */


            if(res.indexOf('||')>=0){
                res='';
            }

            return res?res:null;
        }

        if(isPrevRemoved){

            // this is the replacement of videos instead of addition
            
            const searchBox=document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')

            let currentPlayListHash= getVideoListHash() || null;

            if(!currentPlayListHash){

            }else if(!videoListBeforeSearch && searchBox){

                videoListBeforeSearch= currentPlayListHash;
                if(videoListBeforeSearch){
                    //console.log('fromSearch', videoListBeforeSearch)


                    requestAnimationFrame(function(){


                        let renderer = document.querySelector('[placeholder-videos] ytd-watch-next-secondary-results-renderer');
                        if(searchBox && searchBox.parentNode) searchBox.blur();

                        if(renderer){
                            let scrollParent = renderer.parentNode;
                            if(scrollParent.scrollHeight>scrollParent.offsetHeight){
                                let targetTop = renderer.offsetTop;
                                if(searchBox && searchBox.parentNode==scrollParent ) targetTop-=searchBox.offsetHeight
                                scrollParent.scrollTop= targetTop - scrollParent.firstChild.offsetTop;
                            }
                        }

                        
        
                    });

                }


            }else if(videoListBeforeSearch){

                if(currentPlayListHash != videoListBeforeSearch){

                    videoListBeforeSearch=null;
                    //console.log('fromSearch', videoListBeforeSearch)
    
    
                }

            } 


        }

 
    }




}

function mtf_ChatExist(){

    // no mutation triggering if the changes are inside the iframe 

    // 1) Detection of #continuations inside iframe
    // iframe ownerDocument is accessible due to same origin
    // if the chatroom is collasped, no determination of live chat or replay (as no #continuations and somehow a blank iframe doc)

    // 2) Detection of meta tag
    // This is fastest but not reliable. It is somehow a bug that the navigation might not update the meta tag content
    
    // 3) Detection of HTMLElement inside video player for live video
    
    // (1)+(3) = solution

    
    const elmChat = document.querySelector('ytd-live-chat-frame#chat')
    let elmCont = null;
    if(elmChat){
        elmCont=chatFrameElement('yt-live-chat-renderer #continuations')
    }
    
    const chatBlockR = (elmChat?1:0)+(elmCont?2:0)
    if(Q.mtf_chatBlockQ!==chatBlockR){

        //console.log(897, Q.mtf_chatBlockQ, chatBlockR)
        Q.mtf_chatBlockQ=chatBlockR

        
        const cssElm = ytdFlexy.deref()
        if(elmChat){

            let s=0;
            if(elmCont){
                //not found if it is collasped.
                s |= elmCont.querySelector('yt-timed-continuation')?1:0;
                s |= elmCont.querySelector('yt-live-chat-replay-continuation, yt-player-seek-continuation')?2:0;
                //s |= elmCont.querySelector('yt-live-chat-restricted-participation-renderer')?4:0;
                if(s==1) {
                    cssElm.setAttribute('userscript-chatblock', 'chat-live')
                    requestingComments=null;
                }
                if(s==2) cssElm.setAttribute('userscript-chatblock', 'chat-playback')
                //if(s==5) cssElm.setAttribute('userscript-chatblock', 'chat-live-paid')

                if(s==1) $("span#tab3-txt-loader").text('');

            }
            //keep unknown as original
            if( !cssElm.hasAttribute) cssElm.setAttribute('userscript-chatblock', '')
           

        }else{
            cssElm.removeAttribute('userscript-chatblock')
            cssElm.removeAttribute('userscript-chat-collapsed')

        }
        

    }
}




let lastScrollAt = 0;

function makeBodyScrollByEvt(){
    // inside marco task (event)

    Promise.resolve().then(()=>window.dispatchEvent(new Event("scroll")))


}

function makeBodyScroll() {

    // avoid over triggering scroll event
    if (+new Date - lastScrollAt < 30) return;
    lastScrollAt = +new Date;

    //required for youtube content display

    requestAnimationFrame(()=>{

        window.dispatchEvent(new Event("scroll"));

    })
    


}

let requestingComments = null
function scrollForComments_TF(){
    let comments = requestingComments;
    if ( comments && comments.hasAttribute('hidden')) makeBodyScroll();
}
function scrollForComments() {
    setTimeout(scrollForComments_TF, 80);
    setTimeout(scrollForComments_TF, 240);
    setTimeout(scrollForComments_TF, 870);
}



let mtoNav = null;
let mtoBody = null;


const mtoVs={}


function initObserver(){




    // continuous check for element relocation
    function mtf_append_comments() {
        let comments = document.querySelector('#primary ytd-watch-metadata ~ #info ~ ytd-comments#comments');
        if (comments) $(comments).appendTo('#tab-comments').attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)
    }

    // continuous check for element relocation
    function mtf_liveChatBtnF() {
        let button = document.querySelector('ytd-live-chat-frame#chat>.ytd-live-chat-frame#show-hide-button:nth-child(n+2)');
        if (button) button.parentNode.insertBefore(button, button.parentNode.firstChild)
    }


    
    // continuous check for element relocation
    // fired at begining & window resize, etc
    function mtf_append_playlist(){
        let ple1 = document.querySelector("*:not(#ytd-userscript-playlist)>ytd-playlist-panel-renderer#playlist");
        if(ple1){
            appendWithWrapper(
                ple1,
                'ytd-userscript-playlist',  
                document.querySelector("#tab-list")
            );
            $(ple1).attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)

        }
    }


    // content fix - info & playlist
    // fired at begining, and keep for in case any change
    function mtf_fix_details() {

        const content = document.querySelector('#meta-contents ytd-expander>#content, #tab-info ytd-expander>#content')
        if (content) {
            const expander = content.parentNode;

            if (expander.hasAttribute('collapsed')) setAttr(expander,'collapsed',false);

            let btn1 = expander.querySelector('tp-yt-paper-button#less:not([hidden])');
            let btn2 = expander.querySelector('tp-yt-paper-button#more:not([hidden])');

            if (btn1) setAttr(btn1,'hidden',false);
            if (btn2) setAttr(btn2,'hidden',false);

        }

        // just in case the playlist is collapsed
        const playlist = document.querySelector('#tab-list ytd-playlist-panel-renderer#playlist')
        if(playlist){
            if(playlist.hasAttribute('collapsed')) setAttr(playlist,'collapsed',false);
            if(playlist.hasAttribute('collapsible')) setAttr(playlist,'collapsible',false);
        }        


    }



    let mtoNav_requestNo=0;

    let mtoNav_delayedF = () => {
    
        let {addP, removeP} = Q;
    
        Q.addP = 0;
        Q.removeP = 0;

        

        let promisesForAddition=!scriptEnable?[]:addP > 0?[
            Q.$callOnceAsync('mtf_advancedComments'),
            Q.$callOnceAsync('mtf_infoSectionHeight'),
            Q.$callOnceAsync('mtf_checkDescriptionLoaded'),
            Q.$callOnceAsync('mtf_checkPlayList'),
            Q.$callOnceAsync('mtf_fetchCommentsAvailable'),
            Q.$callOnceAsync('mtf_initalAttr_comments'),
            Q.$callOnceAsync('mtf_initalAttr_playlist'),
            Q.$callOnceAsync('mtf_checkStatus_chatroom'),
            Q.$callOnceAsync('mtf_checkFlexy'),
            Q.$callOnceAsync('mtf_forceCheckLiveVideo'),

            (async () => {
                mtf_append_comments();
            })(),
    
            (async () => {
                mtf_liveChatBtnF();
            })(),

            (async ()=>{
                mtf_fixTabsAtTheEnd();
            })(),
        
            (async () => {
                mtf_append_playlist();
            })()
        ]:[];
        


        let promisesForEveryMutation=!scriptEnable?[]:[
            (async () => {
                mtf_fix_details();
            })(),
            (async () => {
                mtf_ChatExist();
            })()
        ];


        Promise.all([...promisesForAddition,...promisesForEveryMutation]).then(()=>{
            mtoNav_requestNo--;
            //console.log('motnav reduced to', mtoNav_requestNo)
            if(mtoNav_requestNo>0){
                mtoNav_requestNo=1;
                setTimeout(mtoNav_delayedF,mtoInterval);
            }
        })
    
    
    }

    Q.addP=0;
    Q.removeP=0; 
    let hReqNo=0;
    const mtoNavF=(mutations, observer) => {

        let ch = false;
        for (const mutation of mutations) {
            for (const addedNode of mutation.addedNodes)
                if (addedNode.nodeType === 1) {
                    Q.addP++
                    ch = true;
                }
            for (const removedNode of mutation.removedNodes)
                if (removedNode.nodeType === 1) {
                    Q.removeP++;
                    ch = true;
                }
        }
        if (!ch) return;

        mtoNav_requestNo++;
        hReqNo++;
        if(hReqNo==36) {
             mtoInterval=mtoInterval2;
             clickInterval=clickInterval2;
        }
        //console.log('motnav added to', mtoNav_requestNo)

        if(mtoNav_requestNo==1) setTimeout(mtoNav_delayedF,mtoInterval);

    }
    mtoNav = new MutationObserver(mtoNavF);
    mtoNav.observe(ytdFlexy.deref(), {
        subtree: true,
        childList: true
    })

    1;1&&(async()=>{
        Q.addP=1; //fake the function
        mtoNav_requestNo++;
        if(mtoNav_requestNo==1) mtoNav_delayedF();

    })();


    if(mtoBody){
    mtoBody = null;
    }

    function mtoBodyF(mutations, observer){


        for (const mutation of mutations) {
            for (const addedNode of mutation.addedNodes)
                if (addedNode.nodeType === 1) {
                    if(addedNode.nodeName=="DIV" && addedNode.className.indexOf('autocomplete-suggestions')>=0){
                        mtf_fixAutoCompletePosition(addedNode)
                    }
                }
        }



    }

    mtoBody = new MutationObserver(mtoBodyF);
    mtoBody.observe(document.querySelector('body'), {
        childList: true
    })


}

let displayedPlaylist=null
let scrollingVideosList=null

let scriptEnable =false;
let lastShowTab = null;


let _cachedLastVideo=null;
let videoListBeforeSearch=null;
function resetBeforeNav() {
    videoListBeforeSearch=null;
    _cachedLastVideo=null;
    lastShowTab=null;
    displayedPlaylist=null
    scrollingVideosList=null
    scriptEnable =false;
    $ws.layoutStatus=null;


    clearMutationObserver(mtoVs,'mtoVisibility_Playlist')
    clearMutationObserver(mtoVs,'mtoVisibility_Comments')
    clearMutationObserver(mtoVs,'mtoVisibility_Chatroom')
    clearMutationObserver(mtoVs,'mtoFlexyAttr')

    if (mtoBody) {
        mtoBody.takeRecords();
        mtoBody.disconnect();
        mtoBody = null;
    }
    if (mtoNav) {
        mtoNav.takeRecords();
        mtoNav.disconnect();
        mtoNav = null;

        Q.mtf_advancedComments=null;
        Q.mtf_checkDescriptionLoaded = null;
        Q.mtf_checkPlayList = null;
        Q.mtf_fetchCommentsAvailable = null;
        Q.mtf_initalAttr_comments = null;
        Q.mtf_initalAttr_playlist = null;
        Q.mtf_checkStatus_chatroom = null;
        Q.mtf_forceCheckLiveVideo=null;
        Q.mtf_chatBlockQ = null;
    }

    mtoInterval = mtoInterval1;
    clickInterval = clickInterval1;

}

let ytdFlexy=null;
function resetAtNav() {

    scriptEnable =true;

    ytdFlexy = new WeakRefer(document.querySelector('ytd-watch-flexy'))

    $(ytdFlexy.deref()).removeAttr("userscript-chatblock").removeAttr("userscript-chat-collapsed");
    $('#tab-comments').attr('lazy-loading', '');
    $('span#tab3-txt-loader').text('');

    //removed any cache of #comments header (i.e. count message)
    var prevCommentsHeader = document.querySelector('ytd-comments#comments ytd-comments-header-renderer');
    if (prevCommentsHeader) prevCommentsHeader.parentNode.removeChild(prevCommentsHeader);

    var prevCommentsMsg= document.querySelector('ytd-item-section-renderer#sections #header ~ #contents>ytd-message-renderer:only-child');
    if (prevCommentsMsg) prevCommentsMsg.parentNode.removeChild(prevCommentsMsg);

    //force to [hidden]
    var prevComemnts = document.querySelector('ytd-comments#comments'); 
    if (prevComemnts) {
        setAttr(prevComemnts, 'hidden', true);
        requestingComments = prevComemnts;
        //scrollForComments();
    }

    

}

function getTabsHTML(){


    let ts = settings.toggleSettings;

    if (!ts.tabs) return;

    const sTabBtnVideos = `${svgElm(16,16,298,298,svgVideos)}<span>Videos</span>`
    const sTabBtnInfo = `${svgElm(16,16,23.625,23.625,svgInfo)}<span>Info</span>`
    const sTabBtnPlayList = `${svgElm(16,16,426.667,426.667,svgPlayList)}<span>Playlist</span>`

    const str1 = `
     <paper-ripple class="style-scope yt-icon-button">
         <div id="background" class="style-scope paper-ripple" style="opacity:0;"></div>
         <div id="waves" class="style-scope paper-ripple"></div>
     </paper-ripple>
     `;

    const str_tabs = [
        ts.tInfo ? `<a id="tab-btn1" data-name="info" userscript-tab-content="#tab-info" class="tab-btn">${sTabBtnInfo}${str1}</a>` : '',
        `<a id="tab-btn2" userscript-tab-content="#tab-live" class="tab-btn tab-btn-hidden">Chat${str1}</a>`,
        ts.tComments ? `<a id="tab-btn3" userscript-tab-content="#tab-comments" data-name="comments" class="tab-btn">${svgElm(16,16,60,60,svgComments)}<span id="tab3-txt-loader"></span>${str1}</a>` : '',
        ts.tVideos ? `<a id="tab-btn4" userscript-tab-content="#tab-videos" data-name="videos" class="active tab-btn">${sTabBtnVideos}${str1}</a>` : '',
        `<a id="tab-btn5" userscript-tab-content="#tab-list" class="tab-btn">${sTabBtnPlayList}${str1}</a>`
    ].join('')

    var addHTML = `
     <div id="right-tabs">
         <header>
             <div id="material-tabs">
                 ${str_tabs}
             </div>
         </header>
         <div class="tab-content">
             <div id="tab-info" class="tab-content-cld" userscript-scrollbar-render></div>
             <div id="tab-live" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
             <div id="tab-comments" class="tab-content-cld" userscript-scrollbar-render></div>
             <div id="tab-videos" class="tab-content-cld" userscript-scrollbar-render></div>
             <div id="tab-list" class="tab-content-cld" userscript-scrollbar-render></div>
         </div>
     </div>
     `;

     return addHTML

}

function onNavigationEnd() {

    resetBeforeNav();
    if(!/https?\:\/\/(\w+\.)*youtube\.com\/watch\?(\w+\=[^\/\?\&]+\&)*v=[\w\-\_]+/.test(window.location.href))return;
    resetAtNav();


    let promise = Promise.resolve();

    if (!document.querySelector("#right-tabs")) {
        let targetElm = ytdFlexy.deref().querySelector("#secondary>#secondary-inner")||ytdFlexy.deref().querySelector("#secondary")||ytdFlexy.deref().querySelector("#columns");
        if(!targetElm) throw 'Userscript: Two Column flexy layout not found'; // not flexy layout
        promise=promise.then(()=>{
            $(getTabsHTML()).appendTo(targetElm).attr('data-dom-created-by-tabview-youtube',scriptVersionForExternal);
            targetElm=null;
        })
    }

    promise.then(runAfterTabAppended).then(initObserver)

}

function setToActiveTab() {
    if(isTheater() && isWideScreenWithTwoColumns())return;
    const jElm = document.querySelector(`a[userscript-tab-content="${switchTabActivity_lastTab}"]:not(.tab-btn-hidden)`) ||
        document.querySelector(`a[userscript-tab-content="#tab-${settings.defaultTab}"]:not(.tab-btn-hidden)`) ||
        document.querySelector("a[userscript-tab-content]:not(.tab-btn-hidden)") ||
        null;
    switchTabActivity(jElm);
}

function insertBefore(elm, p) {
    if (elm && p && p.parentNode)
        p.parentNode.insertBefore(elm, p);
}

function appendWithWrapper(elm, wrapperId, toParent){
    if(!toParent||!elm)return;
    let $wrapper = $(`#${wrapperId}`);
    if(!$wrapper[0]) $wrapper=$(`<div id="${wrapperId}"></div>`)
    $wrapper.append(elm).appendTo(toParent);
}

function runAfterTabAppended() {

    // just switch to the default tab
    setToActiveTab();

    // append the next videos 
    // it exists as "related" is already here

    fixRelated();



    prepareTabBtn();

    
    // append the detailed meta contents to the tab-info
    Q.mtf_checkDescriptionLoaded = () => {
        const expander = document.querySelector("#meta-contents ytd-expander");
        if (!expander) return true;
        $(expander).appendTo("#tab-info").attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)

        const avatar = ytdFlexy.deref().querySelector('#meta-contents yt-img-shadow#avatar');
        if(avatar) hackImgShadow(avatar)
        return false;
    }
    Q.$callOnceAsync('mtf_checkDescriptionLoaded')

    // force window scroll when #continuations is first detected and #comments still [hidden]
    Q.mtf_advancedComments = () => {
        const continuations = document.querySelector("ytd-comments#comments #continuations");
        if (!continuations) return true;
        requestingComments = document.querySelector('ytd-comments#comments');
        scrollForComments();
        return false;
    }
    Q.$callOnceAsync('mtf_advancedComments')


    // use video player's element to detect the live-chat situation (no commenting section)
    // this would be very useful if the live chat is collapsed, i.e. iframe has no indication on the where it is live or replay
    
    Q.mtf_forceCheckLiveVideo_tf =()=>{
        const cssElm = ytdFlexy.deref()
        if(!cssElm) return;
        if($('#ytd-player .ytp-time-display').is('.ytp-live')) {
            cssElm.setAttribute('userscript-chatblock', 'chat-live')
            requestingComments=null;
        }
    }
    Q.mtf_forceCheckLiveVideo = () => {
        const playerLabel = document.querySelector('#ytd-player .ytp-time-display') && document.querySelector('ytd-live-chat-frame#chat')
        if (!playerLabel) return true;
        setTimeout(Q.mtf_forceCheckLiveVideo_tf,170)
        return false;
    }
    Q.$callOnceAsync('mtf_forceCheckLiveVideo')



    createAttributeObservants();
    checkChatStatus();


    $("#right-tabs [userscript-scrollbar-render]").scroll(makeBodyScrollByEvt);

}


async function asyncFetchCommentsAvailable() {

    let span = document.querySelector("span#tab3-txt-loader")
    if (!span) return;

    makeBodyScroll();

    let fetchedOnce = false
    Q.mtf_fetchCommentsAvailable = () => {

        if(!scriptEnable)return;

        let messageElm, messageStr;
        const commentRenderer = document.querySelector("ytd-comments#comments #count.ytd-comments-header-renderer");
        if (commentRenderer) {
            fetchedOnce=true;
            let r = '0';
            let txt = commentRenderer.textContent
            if (typeof txt == 'string') {
                let m = txt.match(/[\d\,\s]+/)
                if (m) r = m[0].trim()
            }
            span.textContent = r;
            $('#tab-comments[lazy-loading]').removeAttr('lazy-loading')
            mtoInterval=mtoInterval2;
            clickInterval=clickInterval2;
        }else if((messageElm = document.querySelector('ytd-item-section-renderer#sections #header ~ #contents>ytd-message-renderer:only-child'))&&(messageStr=(messageElm.textContent||'').trim())){ //ytd-message-renderer
            // it is possible to get the message before the header generation.
            setTimeout(function(){
                if(fetchedOnce)return;
                const mainMsg= messageElm.querySelector('#message, #submessage')
                if(mainMsg && mainMsg.textContent){
                    for(const msg of mainMsg.querySelectorAll('*:not(:empty)')){
                        if(msg.childElementCount===0 && msg.textContent) {
                            messageStr=msg.textContent.trim()
                            break
                        }
                    } 
                }
                span.textContent = messageStr;
                $('#tab-comments[lazy-loading]').removeAttr('lazy-loading')
            },240);
        }
        return true;
    
    }
    Q.$callOnceAsync('mtf_fetchCommentsAvailable')


}




function createAttributeObservants() {


    // Attr Mutation Observer - #playlist - hidden
    clearMutationObserver(mtoVs,'mtoVisibility_Playlist')
    // Attr Mutation Observer callback - #playlist - hidden
    let mtf_attrPlaylist=(mutations, observer)=>{
        var playlist=document.querySelector('ytd-playlist-panel-renderer#playlist')
        const $tabBtn = $('[userscript-tab-content="#tab-list"]');
        //console.log(3712,$tabBtn)
        //console.log('attr playlist changed')
        if( $tabBtn.is('.tab-btn-hidden') && !playlist.hasAttribute('hidden') ){
            //console.log(3713)
            //console.log('attr playlist changed - no hide')
            $tabBtn.removeClass("tab-btn-hidden");
        }else if( !$tabBtn.is('.tab-btn-hidden') && playlist.hasAttribute('hidden') ){
            //console.log(3714)
            //console.log('attr playlist changed - add hide')
            hideTabBtn($tabBtn);
        }
    }

    // pending for #playlist and set Attribute Observer
    Q.mtf_initalAttr_playlist=()=>{
        var playlist=document.querySelector('ytd-playlist-panel-renderer#playlist')
        if(!playlist) return true;
        initMutationObserver(mtoVs,'mtoVisibility_Playlist', mtf_attrPlaylist)
        mtoVs.mtoVisibility_Playlist.observe(playlist, {          
            attributes: true,
            attributeFilter: ['hidden'],
            attributeOldValue: true
        })
        //console.log(3711)
        mtf_attrPlaylist()
        return false;
    }
    //console.log(3710)
    Q.$callOnceAsync('mtf_initalAttr_playlist')





    // Attr Mutation Observer - ytd-comments#comments - hidden
    
    clearMutationObserver(mtoVs,'mtoVisibility_Comments')
    // Attr Mutation Observer callback - ytd-comments#comments - hidden
    let mtf_attrComments=(mutations, observer)=>{
        var comments=document.querySelector('ytd-comments#comments')
        const $tabBtn = $('[userscript-tab-content="#tab-comments"]');
        if(!comments || !$tabBtn[0])return;
        //console.log('attr comments changed')
        if( $tabBtn.is('.tab-btn-hidden') && !comments.hasAttribute('hidden') ){
            //console.log('attr comments changed - no hide')
            $tabBtn.removeClass("tab-btn-hidden");
            asyncFetchCommentsAvailable();
        }else if( !$tabBtn.is('.tab-btn-hidden') && comments.hasAttribute('hidden') ){
            //console.log('attr comments changed - add hide')
            if(!document.querySelector('[userscript-chatblock="chat-live"]')){
                requestingComments=comments
                $('#tab-comments').attr('lazy-loading','')
            } 
            $('span#tab3-txt-loader').text('');
            hideTabBtn($tabBtn);
        }        
    }

    // pending for #comments and set Attribute Observer
    Q.mtf_initalAttr_comments=()=>{
        var comments=document.querySelector('ytd-comments#comments')
        if(!comments) return true;
        initMutationObserver(mtoVs,'mtoVisibility_Comments',mtf_attrComments)
        mtoVs.mtoVisibility_Comments.observe(comments, {          
            attributes: true,
            attributeFilter: ['hidden'],
            attributeOldValue: true
        })
        mtf_attrComments()
        requestingComments = document.querySelector('ytd-comments#comments');
        scrollForComments()
        return false;
    }
    Q.$callOnceAsync('mtf_initalAttr_comments')

}


function isEmptyBody(){
    //only deal with loaded document without body
    //other situation, case by case

    let iframe = document.querySelector('ytd-live-chat-frame iframe#chatframe');

    if(!iframe) return false; //iframe must be there

    if(iframe.readyState  != 'complete') return false; //we must wait for its completion

    let doc = null;
    try{

        doc=iframe.contentDocument

    }catch(e){}

    if(!doc) return false; //might be not loaded yet

    if(doc.body && doc.body.childElementCount===0){
        //empty body

        return true;
    }


}

class Mutex{
    
    constructor(){
        this.p=Promise.resolve()
    }
    
    lockWith(f){
        
        this.p=this.p.then(()=>{
            return new Promise(f)
        })
    }

}

let layoutStatusMutex=new Mutex();


function checkChatStatus(){
    
    
    clearMutationObserver(mtoVs,'mtoVisibility_Chatroom')

    let cid_chatFrameCheck=0;

    let mtf_attrChatroom=(mutations, observer)=>{



        layoutStatusMutex.lockWith(unlock=>{

            const chatBlock = document.querySelector('ytd-live-chat-frame#chat')
            const cssElm = ytdFlexy.deref()


            
            if(!cssElm.hasAttribute('userscript-chatblock')) setAttr(cssElm, 'userscript-chatblock', true);
            setAttr(cssElm,'userscript-chat-collapsed',!!chatBlock.hasAttribute('collapsed'));

            if(cssElm.hasAttribute('userscript-chatblock')&&!chatBlock.hasAttribute('collapsed')) lastShowTab='#chatroom'


            if(!chatBlock.hasAttribute('collapsed') &&  document.querySelector('#right-tabs .tab-btn.active') && isWideScreenWithTwoColumns() && !isTheater()  ){

                switchTabActivity(null);
    
                setTimeout(unlock,40)

            } else{

                unlock()
            }
    

    
            if( chatBlock && cssElm && cssElm.hasAttribute('userscript-chatblock') && !chatBlock.hasAttribute('collapsed') && !cid_chatFrameCheck){
                let dd=+new Date;
                cid_chatFrameCheck=setInterval(()=>{
                    // mutation on iframe window would not trigger the observer
                    // just check the first few seconds for this purpose.
                    let chatFrameChecking, iframe;
                    if(+new Date - dd>6750){
                        //
                    }else if( isEmptyBody() ){
    
                        // bug. youtube iframe loaded with nothing
    
                        let button = document.querySelector('ytd-live-chat-frame#chat>.ytd-live-chat-frame#show-hide-button ytd-toggle-button-renderer')
                        if (button) {
                            setTimeout(function(){
                                if(button && button.parentNode && isChatExpand()){
                                    button.click();
                                    setTimeout(function(){
                                        if(button && button.parentNode && !isChatExpand()) button.click();
                                        button=null;
                                    },80)
                                }else{
                                    button=null;
                                }
                            },20)
                        } 
    
                    
                    }else if(chatFrameChecking=!!chatFrameElement('yt-live-chat-renderer #continuations')){
                        mtf_ChatExist();
                        $(document.querySelector('ytd-live-chat-frame#chat')).attr('yt-userscript-iframe-loaded','')
                    }else{
                        return;
                    }
                    return (cid_chatFrameCheck=clearInterval(cid_chatFrameCheck));
                },270)
            }else if(chatBlock){
                chatBlock.removeAttribute('yt-userscript-iframe-loaded')
    
            }



        })




    }

    Q.mtf_checkStatus_chatroom=()=>{
        var chatroom=document.querySelector('ytd-live-chat-frame#chat')
        if(!chatroom) return true;
        initMutationObserver(mtoVs,'mtoVisibility_Chatroom',mtf_attrChatroom)
        mtoVs.mtoVisibility_Chatroom.observe(chatroom, {          
            attributes: true,
            attributeFilter: ['collapsed'],
            attributeOldValue: true
        })
        mtf_attrChatroom()
        return false;
    }
    Q.$callOnceAsync('mtf_checkStatus_chatroom')





    clearMutationObserver(mtoVs,'mtoFlexyAttr')


    let mtf_attrFlexy=(mutations, observer)=>{
          


        const cssElm=ytdFlexy.deref()
        ;
        if(!cssElm)return;

        let chatBlockStatusChanged = false
        let theaterStatusChanged = false;
        let twoColStatusChanged = false;
        let initalTriggering = !mutations

        if(!initalTriggering){

            const old_layoutStatus = $ws.layoutStatus
            let new_layoutStatus = old_layoutStatus;
        
            for(const mutation of mutations) {
                if (mutation.attributeName == 'theater') {

                    
                    if(new_layoutStatus!==null){


                        if(isTheater()) new_layoutStatus = new_layoutStatus | LAYOUT_THEATER; 
                        else new_layoutStatus = new_layoutStatus & ~LAYOUT_THEATER;

                    } 

                    theaterStatusChanged=true;
                }else if (mutation.attributeName == 'userscript-chat-collapsed' || mutation.attributeName == 'userscript-chatblock'){

                    if(new_layoutStatus!==null){


                        if(hasAttribute(ytdFlexy.deref(), 'userscript-chatblock')) new_layoutStatus = new_layoutStatus | LAYOUT_CHATROOM; 
                        else new_layoutStatus = new_layoutStatus & ~LAYOUT_CHATROOM;

                        
                        if(hasAttribute(ytdFlexy.deref(), 'userscript-chat-collapsed')) {
                            new_layoutStatus = new_layoutStatus | LAYOUT_CHATROOM; 
                            new_layoutStatus = new_layoutStatus | LAYOUT_CHATROOM_COLLASPED; 
                        }else{
                            new_layoutStatus = new_layoutStatus & ~LAYOUT_CHATROOM_COLLASPED;
                        }


                    } 

                    chatBlockStatusChanged=true
                }else if (mutation.attributeName == 'is-two-columns_'){
                
                    if(new_layoutStatus!==null){


                        if(isWideScreenWithTwoColumns()) new_layoutStatus = new_layoutStatus | LAYOUT_TWO_COLUMNS; 
                        else new_layoutStatus = new_layoutStatus & ~LAYOUT_TWO_COLUMNS;

                    } 

                    twoColStatusChanged=true;
                }else if(mutation.attributeName=='tabview-selection'){
                    
                    if(new_layoutStatus!==null){

                        if(isNonEmptyString( getAttribute(ytdFlexy.deref(),'tabview-selection') )) new_layoutStatus = new_layoutStatus | LAYOUT_TAB_EXPANDED;
                        else new_layoutStatus = new_layoutStatus & ~LAYOUT_TAB_EXPANDED;
                    }
                }else if(mutation.attributeName=='fullscreen'){
                    
                    if(new_layoutStatus!==null){

                        if(isNonEmptyString( getAttribute(ytdFlexy.deref(),'fullscreen') )) new_layoutStatus = new_layoutStatus | LAYOUT_FULLSCREEN;
                        else new_layoutStatus = new_layoutStatus & ~LAYOUT_FULLSCREEN;
                    }
                }
            }


            if(new_layoutStatus!==old_layoutStatus)  $ws.layoutStatus = new_layoutStatus

        }
    







    }


    Q.mtf_checkFlexy=()=>{
        var flexy=ytdFlexy.deref()
        if(!flexy) return true;

        if($ws.layoutStatus===null) $ws.layoutStatus=
        (isWideScreenWithTwoColumns()?LAYOUT_TWO_COLUMNS:0)|(isTheater()?LAYOUT_THEATER:0)|
        (hasAttribute(ytdFlexy.deref(), 'userscript-chatblock')?LAYOUT_CHATROOM:0)|
        (isChatExpand()?LAYOUT_CHATROOM_COLLASPED:0)

        initMutationObserver(mtoVs,'mtoFlexyAttr',mtf_attrFlexy)
        mtoVs.mtoFlexyAttr.observe(flexy, {          
            attributes: true,
            attributeFilter: ['userscript-chat-collapsed','userscript-chatblock','theater','is-two-columns_','tabview-selection','fullscreen'],
            attributeOldValue: true
        })
        mtf_attrFlexy()


        let columns = document.querySelector('ytd-page-manager#page-manager #columns')
        if(columns){
            setAttr(columns, 'userscript-scrollbar-render', true);
        }

        return false;
    }
    Q.$callOnceAsync('mtf_checkFlexy')



}




let switchTabActivity_lastTab = null

function switchTabActivity(activeLink) {


    if (activeLink && $(activeLink).is('.tab-btn-hidden')) return; // not allow to switch to hide tab

    if(isTheater() && isWideScreenWithTwoColumns()) activeLink=null;

    const links = document.querySelectorAll('#material-tabs a[userscript-tab-content]');



    function runAtEnd(){

        if(activeLink) lastShowTab=activeLink.getAttribute('userscript-tab-content')


        //override the default youtube coding event prevention

        //let elm=$('ytd-watch-flexy:not([is-two-columns_]) #tab-list:not(.tab-content-hidden) ytd-playlist-panel-renderer')[0];
        let elm=$('ytd-watch-flexy #tab-list:not(.tab-content-hidden) ytd-playlist-panel-renderer')[0];
        displayedPlaylist=elm;
        if(!!displayedPlaylist) $(ytdFlexy.deref()).attr('userscript-auto-scroll-playlist',''); else $(ytdFlexy.deref()).removeAttr('userscript-auto-scroll-playlist');
        
        scrollingVideosList=$('ytd-watch-flexy #tab-videos:not(.tab-content-hidden) [placeholder-videos]')[0]

        ytdFlexy.deref().setAttribute('tabview-selection',activeLink?lastShowTab:'')
        
    }

    for (const link of links) {
        let content = document.querySelector(link.getAttribute('userscript-tab-content'));
        if (link && content) {
            if (link !== activeLink) {
                $(link).removeClass("active");
                $(content).addClass("tab-content-hidden");
            } else {
                $(link).addClass("active");
                $(content).removeClass("tab-content-hidden");
                window.requestAnimationFrame(() => {
                 
                    content.focus()

                    runAtEnd()
                })
            }
        }
    }

    if(!activeLink){
        runAtEnd();
    }


}

let tabsUiScript_setclick = false;

function prepareTabBtn() {

    const materialTab = document.querySelector("#material-tabs")
    if (!materialTab) return;

    let noActiveTab = !!document.querySelector('ytd-watch-flexy[userscript-chatblock]:not([userscript-chat-collapsed])')

    const activeLink = materialTab.querySelector('a[userscript-tab-content].active:not(.tab-btn-hidden)')
    if (activeLink) switchTabActivity(noActiveTab ? null : activeLink)

    if (!tabsUiScript_setclick) {
        tabsUiScript_setclick = true;
        $(materialTab).on("click", "a", function(evt) {

            if (!this.hasAttribute('userscript-tab-content')) return;



            layoutStatusMutex.lockWith(unlock=>{

                switchTabActivity_lastTab = this.getAttribute('userscript-tab-content');

                if( isWideScreenWithTwoColumns() && !isTheater() && $(this).is(".tab-btn.active:not(.tab-btn-hidden)")){

                    ytBtnSetTheater();

                    setTimeout(unlock,20)

                }else if($(this).is(".tab-btn.active:not(.tab-btn-hidden)")){
                

                    switchTabActivity(null);
                    setTimeout(unlock,20)


                }else{

                    new Promise(requestAnimationFrame).then(() => {
                        if(isChatExpand() && isWideScreenWithTwoColumns()) ytBtnCollapseChat();
                        else if(isWideScreenWithTwoColumns() && isTheater() ) ytBtnCancelTheater();
                    }).then(() => {
                        setTimeout(()=>{
                            switchTabActivity(this)
                            
                            setTimeout(makeBodyScroll,20); // this is to make the image render


                            setTimeout(()=>{
                                let rightTabs=document.querySelector('#right-tabs');
                                if(!isWideScreenWithTwoColumns() && rightTabs && rightTabs.offsetTop>0 && $(this).is('.active')){

                                    window.scrollTo(0, rightTabs.offsetTop);
                                
                                }
                                
                                setTimeout(unlock,20)
                            },60)

                        }, clickInterval);
                    })

                }


            })
            

            evt.preventDefault();
        });

    }

}


// ---------------------------------------------------------------------------------------------
window.addEventListener("yt-navigate-finish", onNavigationEnd)

const singleColumnScrolling = (function() {
    var lastD = 0,
        lastF = 0;

    return function() {
        let pageY = pageYOffset;
        if (pageY < 10 && lastD === 0 && !lastF) return;

        let targetElm, header, navElm;

        Promise.resolve().then(() => {

            targetElm = document.querySelector("#right-tabs");
            if (!targetElm) return;
            header = targetElm.querySelector("header");
            if (!header) return;
            navElm = document.querySelector('#masthead-container, #masthead')
            if (!navElm) return;
            navHeight = navElm ? navElm.offsetHeight : 0

            let elmY = targetElm.offsetTop

            let xyz = [elmY + navHeight, pageY, elmY - navHeight]

            let xyStatus = 0
            if (xyz[1] < xyz[2] && xyz[2] < xyz[0]) {
                // 1
                xyStatus = 1
            }

            if (xyz[0] > xyz[1] && xyz[1] > xyz[2]) {

                //2
                xyStatus = 2

            }

            if (xyz[2] < xyz[0] && xyz[0] < xyz[1]) {
                // 3

                xyStatus = 3


            }

            return xyStatus;

        }).then((xyStatus) => {

            if ((xyStatus == 2 || xyStatus == 3) && (lastD === 0 || lastF)) {
                lastD = 1;
                let {
                    offsetHeight
                } = header
                let {
                    offsetWidth
                } = targetElm

                targetElm.style.setProperty("--userscript-sticky-width", offsetWidth + 'px')
                targetElm.style.setProperty("--userscript-sticky", offsetHeight + 'px')
                
                setAttr(targetElm, 'userscript-sticky', true);

            } else if ((xyStatus == 1) && (lastD === 1 || lastF)) {
                lastD = 0;

                setAttr(targetElm, 'userscript-sticky', false);
            }


            targetElm = null;
            header = null;
            navElm = null;

        })

    }
})();

window.addEventListener("scroll", function() {
    if(!scriptEnable)return;
    singleColumnScrolling()
}, {
    capture: false,
    passive: true
})

var lastTheatreStatus = 0
window.addEventListener('resize', function() {
    if(!scriptEnable)return;

    requestAnimationFrame(() => {
        lastF = 1;
        singleColumnScrolling()
        lastF = 0;
    })

}, {
    capture: false,
    passive: true
})

window.addEventListener('beforeunload', function() {
    if(!scriptEnable)return;
    console.log('beforeunload')
    resetBeforeNav();
    let video=document.querySelector('video');
    if(video && !video.paused) video.pause(); 
}, {capture: true})

window.addEventListener('hashchange', function() { 
    if(!scriptEnable)return;
    console.log('hashchange')
    resetBeforeNav();
  }, {capture: true})
  
  window.addEventListener('popstate', function() { 
    if(!scriptEnable)return;
    console.log('popstate')
    resetBeforeNav();
  }, {capture: true})


function clearMutationObserver(o, key){
    if(o[key]) {
        o[key].takeRecords();
        o[key].disconnect();
        o[key]=null;
    }
}

function initMutationObserver(o, key, callback){


    clearMutationObserver(o,key)
    o[key]=new MutationObserver(callback)



}

document.addEventListener('wheel',function(evt){
    
    if(!scriptEnable)return;
    if(displayedPlaylist && displayedPlaylist.contains(evt.target)){
      evt.stopPropagation(); evt.stopImmediatePropagation()
    }
},{capture:true,passive:true});


function setVideosTwoColumns(flag, bool){

//two columns to one column

/*
    [placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy

    is-two-columns =""  => no is-two-columns
    
    
    
    [placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer
    
    no hidden => hidden =""
    
    
    [placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer
    
    
    hidden ="" => no hidden
    

    */

    let cssSelector1='[placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy'

    let cssSelector2='[placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer'

    let cssSelector3='[placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer'

    let res={}
    if(flag&1){

        res.m1=$(cssSelector1)[0]
    if(bool) $(res.m1).attr('is-two-columns',''); else $(res.m1).removeAttr('is-two-columns')
    }

    if(flag&2){
        res.m2=$(cssSelector2)[0]
    if(bool) $(res.m2).removeAttr('hidden'); else $(res.m2).attr('hidden','')
    }

    if(flag&4){
        res.m3=$(cssSelector3)[0]
    if(bool) $(res.m3).attr('hidden',''); else $(res.m3).removeAttr('hidden');
    }


    return res
    
    
    


}
/*
document.addEventListener('column1',function(evt){

    console.log(evt)
    document.aab=setVideosTwoColumns(1|2|4, false);
})
document.addEventListener('column2',function(evt){

    console.log(evt)
    document.aab=setVideosTwoColumns(1|2|4, true);
})
*/

let lastScrollFetch=0;
function isScrolledToEnd(){
    return (window.innerHeight + window.pageYOffset) >= document.scrollingElement.scrollHeight - 2;
}
let lastOffsetTop = 0;
window.addEventListener('scroll',function(evt){

    //console.log(evt.target)

    if(!scriptEnable)return;


    if( !scrollingVideosList ) return;
    if( videoListBeforeSearch ) return;



    let visibleHeight = document.scrollingElement.clientHeight;
    let totalHeight = document.scrollingElement.scrollHeight;

    if(totalHeight<visibleHeight*1.5)return; // filter out two column view;

    let z = window.pageYOffset + visibleHeight;
    let h_advanced = totalHeight - ( visibleHeight > 5*40 ? visibleHeight*0.5 : 40 );



    if(z>h_advanced && !isWideScreenWithTwoColumns() ){

        if(new Date -  lastScrollFetch<500) return; //prevent continuous calling

        lastScrollFetch=+new Date;
    
        let res= setVideosTwoColumns(2|4, true)
        if(res.m3 && res.m2){

            //wait for DOM change, just in case
            requestAnimationFrame(()=>{
                let {offsetTop}=res.m2  // as visiblity of m2 & m3 switched.

                if(offsetTop-lastOffsetTop<40) return; // in case bug, or repeating calling.  // the next button shall below the this button 
                lastOffsetTop= offsetTop

                res.m2.parentNode.dispatchEvent(new Event('yt-service-request-sent-button-renderer'))
         
                res= null
            }) 

        }else{
            
            res= null
        }


    }




},{passive:true})

/*
function injection01(){
 
    function getVideoListHash(){

        let elms = document.querySelectorAll('[placeholder-videos] #items ytd-compact-video-renderer')
        
        let res = [...elms].map(elm=>elm.data.videoId||'').join('|') ;
        if(res.indexOf('||')>=0){
            res='';
        }

        return res?res:null;
    }

    document.addEventListener('getVideoListHash_listen',function(){


        document.dispatchEvent(new CustomEvent('getVideoListHash_callback',{detail:getVideoListHash()}))

    },true)

}

addScript(`(${injection01+''})()`);
*/


















    // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js

}


;!(function $$() {
    'use strict';

    if(document.documentElement==null) return window.requestAnimationFrame($$)

var cssTxt = GM_getResourceText("contentCSS");

function addStyle (styleText) {
  const styleNode = document.createElement('style');
  styleNode.type = 'text/css';
  styleNode.textContent = styleText;
  document.documentElement.appendChild(styleNode);
  return styleNode;
}


addStyle (cssTxt);

    main(window.$);


    // Your code here...
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址