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