您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Make comments and lists into tabs
当前为
// ==UserScript== // @name Tabview Youtube // @namespace http://tampermonkey.net/ // @version 0.5 // @description Make comments and lists into tabs // @author CY Fung // @match https://www.youtube.com/watch?v=* // @resource contentCSS https://github.com/cyfung1031/Tabview-Youtube/raw/main/css/style_content.css?v20210702a // @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 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"/> ` 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 isVideoPlaying(video) { return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > video.HAVE_CURRENT_DATA; } function hideTabBtn($tabBtn){ var isActiveBefore = $tabBtn.is('.active') $tabBtn.addClass("tab-btn-hidden"); if (isActiveBefore) { setToActiveTab(); } } function isTheater(){ const cssElm=document.querySelector('ytd-watch-flexy'); return (cssElm && cssElm.hasAttribute('theater')) } function isChatExpand(){ const cssElm=document.querySelector('ytd-watch-flexy'); return cssElm && cssElm.hasAttribute('userscript-chatblock') && !cssElm.hasAttribute('userscript-chat-collapsed') } function isWideScreenWithTwoColumns(){ const cssElm=document.querySelector('ytd-watch-flexy'); return (cssElm && cssElm.hasAttribute('is-two-columns_')) } function isAnyActiveTab(){ return $('#right-tabs .tab-btn.active').length>0 } function ytBtnCancelTheater(){ if(isTheater()){ const sizeBtn = document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button') if(sizeBtn) sizeBtn.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 fixDisplayForTheaterModeChanged(){ const cssElm = document.querySelector('ytd-watch-flexy') if(!cssElm) return; if(isTheater() && isWideScreenWithTwoColumns()){ if( isAnyActiveTab()) switchTabActivity(null) if( isChatExpand() ) ytBtnCollapseChat() }else if( !isTheater() && !isChatExpand() && !isAnyActiveTab()){ setToActiveTab(); }else if( !isWideScreenWithTwoColumns() && !isChatExpand() && !isAnyActiveTab() ){ setToActiveTab(); } } 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(){ img.removeEventListener('load',onload,false) if(p&&imgShadow&&z) 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 elm = null; try{ elm = iframe.contentDocument.querySelector(cssSelector) }catch(e){ console.log('iframe error', e) } return elm; } 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 let nonLastRightTabs = document.querySelector('#secondary #right-tabs:not(:last-child)') if(nonLastRightTabs){ nonLastRightTabs.parentNode.appendChild(nonLastRightTabs) } } 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 = document.querySelector('ytd-watch-flexy') 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') 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")); }) } function scrollForComments() { let comments = document.querySelector('ytd-comments#comments'); function f() { if (comments.hasAttribute('hidden')) makeBodyScroll(); } setTimeout(f, 80); setTimeout(f, 240); setTimeout(f, 870); } let mtoNav = 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') } // 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") ); } } // 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')) expander.removeAttribute('collapsed'); let btn1 = expander.querySelector('tp-yt-paper-button#less:not([hidden])'); let btn2 = expander.querySelector('tp-yt-paper-button#more:not([hidden])'); if (btn1) btn1.setAttribute('hidden', ''); if (btn2) btn2.setAttribute('hidden', ''); } // just in case the playlist is collapsed const playlist = document.querySelector('#tab-list ytd-playlist-panel-renderer#playlist') if(playlist){ if(playlist.hasAttribute('collapsed')) playlist.removeAttribute('collapsed'); if(playlist.hasAttribute('collapsible')) playlist.removeAttribute('collapsible'); } } let mtoNav_requestNo=0; let mtoNav_delayedF = () => { let {addP, removeP} = Q; Q.addP = 0; Q.removeP = 0; let promisesForAddition=addP > 0?[ Q.$callOnceAsync('mtf_advancedComments'), 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=[ (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(document.querySelector('ytd-watch-flexy'), { subtree: true, childList: true }) 1;1&&(async()=>{ Q.addP=1; //fake the function mtoNav_requestNo++; if(mtoNav_requestNo==1) mtoNav_delayedF(); })(); } function resetAtNav() { 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; $("ytd-watch-flexy").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); //force to [hidden] var prevComemnts = document.querySelector('ytd-comments#comments'); if (prevComemnts) { document.querySelector('ytd-comments#comments').setAttribute('hidden', ''); 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() { if (window.location.href.indexOf("www.youtube.com/watch?v=") < 0) return; resetAtNav(); let promise = Promise.resolve(); if (!document.querySelector("#right-tabs")) { let targetElm = document.querySelector("ytd-watch-flexy #secondary>#secondary-inner")||document.querySelector("ytd-watch-flexy #secondary")||document.querySelector("ytd-watch-flexy #columns"); if(!targetElm) throw 'Userscript: Two Column flexy layout not found'; // not flexy layout promise=promise.then(()=>{ $(getTabsHTML()).appendTo(targetElm); 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 let relatedVideos = document.querySelector("#related>ytd-watch-next-secondary-results-renderer"); if(relatedVideos){ appendWithWrapper( relatedVideos, 'ytd-userscript-watch-next-videos', document.querySelector("#tab-videos") ); } 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") const avatar = document.querySelector('ytd-watch-flexy #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; scrollForComments(); return false; } Q.$callOnceAsync('mtf_advancedComments') // make window scroll event from playlist scrolling // i guess it shall be not neccessary, just in case Q.mtf_checkPlayList = () => { const items= document.querySelector('ytd-playlist-panel-renderer>#container>#items'); if(!items) return true; $(items).scroll(makeBodyScrollByEvt); return false; } Q.$callOnceAsync('mtf_checkPlayList') // 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 = () => { const playerLabel = document.querySelector('#ytd-player .ytp-time-display') && document.querySelector('ytd-live-chat-frame#chat') if (!playerLabel) return true; setTimeout(function(){ const cssElm = document.querySelector('ytd-watch-flexy') if(!cssElm) return; if($('#ytd-player .ytp-time-display').is('.ytp-live')) cssElm.setAttribute('userscript-chatblock', 'chat-live') },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(); Q.mtf_fetchCommentsAvailable = () => { let messageElm, messageStr; const commentRenderer = document.querySelector("ytd-comments#comments #count.ytd-comments-header-renderer"); if (commentRenderer) { 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-comments#comments #contents>*:only-child"))&&(messageStr=(messageElm.textContent||'').trim())){ //ytd-message-renderer 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') } return true; } Q.$callOnceAsync('mtf_fetchCommentsAvailable') } function createAttributeObservants() { // Attr Mutation Observer - #playlist - hidden if(mtoVs.mtoVisibility_Playlist) { mtoVs.mtoVisibility_Playlist.takeRecords(); mtoVs.mtoVisibility_Playlist.disconnect(); mtoVs.mtoVisibility_Playlist=null; } // 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('attr playlist changed') if( $tabBtn.is('.tab-btn-hidden') && !playlist.hasAttribute('hidden') ){ //console.log('attr playlist changed - no hide') $tabBtn.removeClass("tab-btn-hidden"); }else if( !$tabBtn.is('.tab-btn-hidden') && playlist.hasAttribute('hidden') ){ //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; mtoVs.mtoVisibility_Playlist=new MutationObserver(mtf_attrPlaylist) mtoVs.mtoVisibility_Playlist.observe(playlist, { attributes: true, attributeFilter: ['hidden'], attributeOldValue: true }) mtf_attrPlaylist() return false; } Q.$callOnceAsync('mtf_initalAttr_playlist') // Attr Mutation Observer - ytd-comments#comments - hidden if(mtoVs.mtoVisibility_Comments) { mtoVs.mtoVisibility_Comments.takeRecords(); mtoVs.mtoVisibility_Comments.disconnect(); mtoVs.mtoVisibility_Comments=null; } // 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"]')) $('#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; mtoVs.mtoVisibility_Comments=new MutationObserver(mtf_attrComments) mtoVs.mtoVisibility_Comments.observe(comments, { attributes: true, attributeFilter: ['hidden'], attributeOldValue: true }) mtf_attrComments() scrollForComments() return false; } Q.$callOnceAsync('mtf_initalAttr_comments') } function checkChatStatus(){ if(mtoVs.mtoVisibility_Chatroom) { mtoVs.mtoVisibility_Chatroom.takeRecords(); mtoVs.mtoVisibility_Chatroom.disconnect(); mtoVs.mtoVisibility_Chatroom=null; } let cid_chatFrameCheck=0; let mtf_attrChatroom=(mutations, observer)=>{ const chatBlock = document.querySelector('ytd-live-chat-frame#chat') const cssElm = document.querySelector('ytd-watch-flexy') if(!cssElm.hasAttribute('userscript-chatblock')) cssElm.setAttribute('userscript-chatblock', ''); if (chatBlock.hasAttribute('collapsed')) { cssElm.setAttribute('userscript-chat-collapsed', ''); } else { cssElm.removeAttribute('userscript-chat-collapsed'); } if(!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. if(+new Date - dd>6750) { return (cid_chatFrameCheck=clearInterval(cid_chatFrameCheck)); } var chatFrameChecking=!!chatFrameElement('yt-live-chat-renderer #continuations') if(chatFrameChecking) { mtf_ChatExist(); return (cid_chatFrameCheck=clearInterval(cid_chatFrameCheck)); } },270) } } Q.mtf_checkStatus_chatroom=()=>{ var chatroom=document.querySelector('ytd-live-chat-frame#chat') if(!chatroom) return true; mtoVs.mtoVisibility_Chatroom=new MutationObserver(mtf_attrChatroom) mtoVs.mtoVisibility_Chatroom.observe(chatroom, { attributes: true, attributeFilter: ['collapsed'], attributeOldValue: true }) mtf_attrChatroom() return false; } Q.$callOnceAsync('mtf_checkStatus_chatroom') if(mtoVs.mtoFlexyAttr) { mtoVs.mtoFlexyAttr.takeRecords(); mtoVs.mtoFlexyAttr.disconnect(); mtoVs.mtoFlexyAttr=null; } let mtf_attrFlexy=(mutations, observer)=>{ const cssElm=document.querySelector('ytd-watch-flexy'); if(!cssElm)return; let chatBlockStatusChanged = false let theaterStatusChanged = false; let twoColStatusChanged = false; let initalTriggering = !mutations if(!initalTriggering){ for(const mutation of mutations) { if (mutation.attributeName == 'theater') { theaterStatusChanged=true; } if (mutation.attributeName == 'userscript-chat-collapsed' || 'userscript-chatblock'){ chatBlockStatusChanged=true } if (mutation.attributeName == 'is-two-columns_'){ twoColStatusChanged=true; } } } if(theaterStatusChanged || initalTriggering){ requestAnimationFrame(fixDisplayForTheaterModeChanged) } if((chatBlockStatusChanged && !theaterStatusChanged) || initalTriggering){ //chatroom is shown or hidden let isOpenChatFrame = !cssElm.hasAttribute('userscript-chat-collapsed') && cssElm.hasAttribute('userscript-chatblock') window.requestAnimationFrame(()=>{ if (isOpenChatFrame && !isTheater()) { switchTabActivity(null) } else if(!isOpenChatFrame && !isTheater()){ setToActiveTab(); } else if(isTheater() && isWideScreenWithTwoColumns() && isOpenChatFrame && isWideScreenWithTwoColumns()){ ytBtnCancelTheater(); } }) } if((twoColStatusChanged && !theaterStatusChanged) || initalTriggering){ fixDisplayForTheaterModeChanged(); } } Q.mtf_checkFlexy=()=>{ var flexy=document.querySelector('ytd-watch-flexy') if(!flexy) return true; mtoVs.mtoFlexyAttr=new MutationObserver(mtf_attrFlexy) mtoVs.mtoFlexyAttr.observe(flexy, { attributes: true, attributeFilter: ['userscript-chat-collapsed','userscript-chatblock','theater','is-two-columns_'], attributeOldValue: true }) mtf_attrFlexy() 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]'); 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()) } } } } 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; switchTabActivity_lastTab = this.getAttribute('userscript-tab-content'); if( isWideScreenWithTwoColumns() && !isTheater() && $(this).is(".tab-btn.active:not(.tab-btn-hidden)")){ const sizeBtn=document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button') if(sizeBtn) sizeBtn.click(); }else if($(this).is(".tab-btn.active:not(.tab-btn-hidden)")){ switchTabActivity(null); }else{ window.requestAnimationFrame(() => { Promise.resolve().then(() => { if(isChatExpand()) ytBtnCollapseChat(); else if(isWideScreenWithTwoColumns() && isTheater() ) ytBtnCancelTheater(); }).then(() => { setTimeout(() => { switchTabActivity(this) setTimeout(() => { makeBodyScroll(); },20); }, clickInterval); }) }) } evt.preventDefault(); }); } } // --------------------------------------------------------------------------------------------- window.addEventListener("yt-navigate-finish", onNavigationEnd) var lastC=0, lastD=0, lastF=0; function singleColumnScrolling(){ let pageY=pageYOffset; if(pageY<10 && lastD===0 && !lastF)return; let targetElm = document.querySelector("#right-tabs"); if(!targetElm) return; let header = targetElm.querySelector("header"); if(!header) return; let navElm = document.querySelector('#masthead-container, #masthead') if(!navElm) return; let 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 } 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') targetElm.setAttribute("userscript-sticky","") }else if( (xyStatus==1)&&(lastD===1 || lastF)){ lastD=0; targetElm.removeAttribute("userscript-sticky") } } window.addEventListener("scroll",function(){ singleColumnScrolling() },{capture:false, passive:true}) var lastTheatreStatus=0 window.addEventListener('resize',function(){ requestAnimationFrame(()=>{ lastF=1; singleColumnScrolling() lastF=0; }) //setTimeout(singleColumnScrolling,40) //setTimeout(singleColumnScrolling,80) //setTimeout(singleColumnScrolling,120) //setTimeout(singleColumnScrolling,270) },{capture:false, passive:true}) // 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... })();