Tabview Youtube

Make comments and lists into tabs

当前为 2021-07-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Tabview Youtube
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6
  5. // @description Make comments and lists into tabs
  6. // @author CY Fung
  7. // @match https://www.youtube.com/watch?v=*
  8. // @resource contentCSS https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube/b57d5c149caf4b78df3eeb0b9a791af8347d97cb/css/style_content.css
  9. // @icon https://github.com/cyfung1031/Tabview-Youtube/raw/main/images/icon128p.png
  10. // @require https://code.jquery.com/jquery-3.6.0.slim.min.js
  11. // @grant GM_getResourceText
  12. // @run-at document-start
  13. // @license MIT https://github.com/cyfung1031/Tabview-Youtube/blob/main/LICENSE
  14. // ==/UserScript==
  15. function main($){
  16. // MIT License
  17. // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js
  18.  
  19.  
  20.  
  21.  
  22.  
  23. /**
  24. * SVG resources:
  25. * <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>
  26. */
  27.  
  28. const scriptVersionForExternal = '2021/07/03';
  29.  
  30. const svgComments = `
  31. <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
  32. 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
  33. 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
  34. 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
  35. 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"/>
  36. <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
  37. 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
  38. c3.27,0,5.93-2.66,5.93-5.929V8.465C60,5.195,57.34,2.535,54.072,2.535z"/>
  39. `
  40.  
  41. 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
  42. z M91,39h43v34H91V39z M61,259H30v-34h31V259z M61,73H30V39h31V73z M134,259H91v-34h43V259z M123,176.708v-55.417
  43. 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
  44. C128.84,188.011,123,184.958,123,176.708z M207,259h-43v-34h43V259z M207,73h-43V39h43V73z M268,259h-31v-34h31V259z M268,73h-31V39
  45. h31V73z"/>`
  46.  
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  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
  54. 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
  55. 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
  56. 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
  57. 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
  58. c0.353,0.331,0.53,0.731,0.53,1.196C14.667,6.703,14.49,7.101,14.137,7.429z"/>`
  59.  
  60. const svgPlayList = `
  61. <rect x="0" y="64" width="256" height="42.667"/>
  62. <rect x="0" y="149.333" width="256" height="42.667"/>
  63. <rect x="0" y="234.667" width="170.667" height="42.667"/>
  64. <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
  65. 298.667,277.333 298.667,362.667 341.333,362.667 341.333,277.333 426.667,277.333 426.667,234.667"/>
  66. `
  67.  
  68.  
  69.  
  70.  
  71.  
  72. const svgElm = (w, h, vw, vh, p) => `<svg width="${w}" height="${h}" viewBox="0 0 ${vw} ${vh}" preserveAspectRatio="xMidYMid meet">${p}</svg>`
  73.  
  74. let settings = {
  75. toggleSettings: {
  76. tabs: 1,
  77. tInfo: 1,
  78. tComments: 1,
  79. tVideos: 1,
  80. },
  81. defaultTab: "videos"
  82. };
  83.  
  84. const mtoInterval1=40;
  85. const mtoInterval2=150;
  86.  
  87. const clickInterval1=100;
  88. const clickInterval2=30;
  89.  
  90. let mtoInterval = mtoInterval1;
  91. let clickInterval=clickInterval1;
  92.  
  93. function isVideoPlaying(video) {
  94. return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > video.HAVE_CURRENT_DATA;
  95. }
  96.  
  97. function setAttr(elm, attrName, b){
  98.  
  99. if(!elm)return;
  100. if(b) elm.setAttribute(attrName,''); else elm.removeAttribute(attrName);
  101. }
  102.  
  103. function hideTabBtn($tabBtn){
  104. var isActiveBefore = $tabBtn.is('.active')
  105.  
  106. $tabBtn.addClass("tab-btn-hidden");
  107. if (isActiveBefore) {
  108. setToActiveTab();
  109. }
  110. }
  111.  
  112. function isTheater(){
  113. const cssElm=document.querySelector('ytd-watch-flexy');
  114. return (cssElm && cssElm.hasAttribute('theater'))
  115. }
  116.  
  117. function isChatExpand(){
  118. const cssElm=document.querySelector('ytd-watch-flexy');
  119. return cssElm && cssElm.hasAttribute('userscript-chatblock') && !cssElm.hasAttribute('userscript-chat-collapsed')
  120. }
  121. function isWideScreenWithTwoColumns(){
  122. const cssElm=document.querySelector('ytd-watch-flexy');
  123. return (cssElm && cssElm.hasAttribute('is-two-columns_'))
  124. }
  125.  
  126. function isAnyActiveTab(){
  127. return $('#right-tabs .tab-btn.active').length>0
  128. }
  129.  
  130. function ytBtnCancelTheater(){
  131. if(isTheater()){
  132. const sizeBtn = document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
  133. if(sizeBtn) sizeBtn.click();
  134. }
  135. }
  136.  
  137. function ytBtnExpandChat(){
  138. let button = document.querySelector('ytd-live-chat-frame#chat[collapsed]>.ytd-live-chat-frame#show-hide-button')
  139. if (button) button.querySelector('ytd-toggle-button-renderer').click();
  140. }
  141. function ytBtnCollapseChat(){
  142. let button = document.querySelector('ytd-live-chat-frame#chat:not([collapsed])>.ytd-live-chat-frame#show-hide-button')
  143. if (button) button.querySelector('ytd-toggle-button-renderer').click();
  144. }
  145.  
  146. function fixDisplayForTheaterModeChanged(){
  147. const cssElm = document.querySelector('ytd-watch-flexy')
  148. if(!cssElm) return;
  149. if(isTheater() && isWideScreenWithTwoColumns()){
  150. if( isAnyActiveTab()) switchTabActivity(null)
  151. if( isChatExpand() ) ytBtnCollapseChat()
  152.  
  153. }else if( !isTheater() && !isChatExpand() && !isAnyActiveTab()){
  154.  
  155. console.log('a112', lastShowTab)
  156. if(lastShowTab=='#chatroom') ytBtnExpandChat(); else setToActiveTab();
  157.  
  158. }else if( !isWideScreenWithTwoColumns() && !isChatExpand() && !isAnyActiveTab() ){
  159. setToActiveTab();
  160.  
  161. }
  162. }
  163.  
  164. function hackImgShadow(imgShadow){
  165. // add to #columns and add back after loaded
  166. let img = imgShadow.querySelector('img')
  167. if(!img)return;
  168.  
  169. let p=imgShadow.parentNode
  170. let z=$(imgShadow).clone()[0]; //to occupy the space
  171. p.replaceChild(z, imgShadow)
  172. $(imgShadow).prependTo('#columns'); // refer to css hack
  173.  
  174. function onload(evt){
  175. if(evt) this.removeEventListener('load',onload,false)
  176. p.replaceChild(imgShadow, z)
  177. p=null;
  178. z=null;
  179. imgShadow=null;
  180. }
  181.  
  182. if (img.complete) onload();
  183. else img.addEventListener('load',onload,false)
  184. }
  185.  
  186.  
  187. const Q={}
  188.  
  189. Q.$callOnceAsync=async function(key){
  190. if (Q[key] && Q[key]() === false) Q[key] = null
  191. }
  192.  
  193. function chatFrameElement(cssSelector){
  194. let iframe = document.querySelector('iframe#chatframe');
  195. if(!iframe) return null;
  196. let cDoc = iframe.contentDocument;
  197. if(!cDoc) return null;
  198. if(cDoc.readyState != 'complete') return null; //we must wait for its completion
  199. let elm = null;
  200. try{
  201. elm = cDoc.querySelector(cssSelector)
  202. }catch(e){
  203. console.log('iframe error', e)
  204. }
  205. return elm;
  206. }
  207.  
  208.  
  209. function fixRelated(){
  210. if(!document.querySelector("#tab-videos>[placeholder-videos]>ytd-watch-next-secondary-results-renderer[data-dom-changed-by-tabview-youtube]")){
  211.  
  212.  
  213.  
  214. let relatedVideos = document.querySelector("#related>ytd-watch-next-secondary-results-renderer");
  215. if(relatedVideos){
  216.  
  217. $('[placeholder-videos]').removeAttr('placeholder-videos')
  218. $('[placeholder-for-youtube-play-next-queue]').removeAttr('placeholder-for-youtube-play-next-queue')
  219.  
  220. let $parentNode= $(relatedVideos.parentNode).appendTo(document.querySelector("#tab-videos"))
  221. $(relatedVideos).attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)
  222.  
  223. $parentNode.attr('placeholder-for-youtube-play-next-queue','').attr('placeholder-videos','')
  224.  
  225. $('[placeholder-videos]').scroll(makeBodyScrollByEvt);
  226.  
  227. }
  228.  
  229. }
  230. }
  231.  
  232. function extractTextContent(elm){
  233. 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,'')
  234. }
  235.  
  236. function mtf_fixTabsAtTheEnd(){
  237. // if window resize, youtube coding will relocate the element
  238. // for example, chatroom move before #right-tabs
  239. // causing difference apperance after resize of window
  240.  
  241.  
  242. fixRelated();
  243.  
  244. let nonLastRightTabs = document.querySelector('#secondary #right-tabs:not(:last-child)')
  245.  
  246. if(nonLastRightTabs){
  247. nonLastRightTabs.parentNode.appendChild(nonLastRightTabs)
  248. }
  249.  
  250. let chatroom = document.querySelector('#primary ytd-live-chat-frame#chat');
  251. if(chatroom){
  252. let right_tabs = document.querySelector('#secondary #right-tabs:last-child')
  253. if(right_tabs){
  254.  
  255.  
  256. right_tabs.parentNode.insertBefore(chatroom, right_tabs)
  257.  
  258. }
  259. }
  260.  
  261.  
  262.  
  263. const autocomplete=document.querySelector('body>.autocomplete-suggestions:not([position-fixed-by-tabview-youtube]):not(:empty)')
  264. if(autocomplete){
  265. const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  266.  
  267. if(searchBox){
  268.  
  269.  
  270. autocomplete.setAttribute('position-fixed-by-tabview-youtube','');
  271.  
  272.  
  273.  
  274. if(!searchBox.hasAttribute('is-set-click-to-toggle')){
  275. searchBox.setAttribute('is-set-click-to-toggle','')
  276. searchBox.addEventListener('click',function(){
  277.  
  278. setTimeout(function(){
  279. let elm=document.querySelector('.autocomplete-suggestions[position-fixed-by-tabview-youtube]:not(:empty)')
  280.  
  281. $(elm).toggle()
  282. //if(elm.style.display=='none') elm.style.display=''; else elm.style.display='none';
  283. },100);
  284.  
  285. })
  286. }
  287.  
  288.  
  289. let aaa=searchBox.nextSibling;
  290. if(aaa && aaa.nodeName=="BFJQ"){
  291. }else if(aaa && aaa.nodeName!="BFJQ"){
  292.  
  293. $(aaa=document.createElement("BFJQ")).insertAfter(searchBox);
  294. }else{
  295.  
  296. $(aaa=document.createElement("BFJQ")).prependTo(searchBox.parentNode);
  297.  
  298.  
  299. }
  300. $(autocomplete).prependTo(aaa);
  301.  
  302.  
  303. aaa.style.setProperty('--sb-margin-bottom',getComputedStyle(searchBox).marginBottom)
  304. aaa.style.setProperty('--height',searchBox.offsetHeight + 'px')
  305.  
  306. /*
  307. setInterval(function(){
  308.  
  309. autocomplete.style.setProperty('--ac-left',autocomplete.getBoundingClientRect().left + 'px')
  310. autocomplete.style.setProperty('--ac-top',autocomplete.getBoundingClientRect().top + 'px')
  311. autocomplete.style.setProperty('--sb-left',aaa.getBoundingClientRect().left + 'px')
  312. autocomplete.style.setProperty('--sb-top',aaa.getBoundingClientRect().top + 'px')
  313.  
  314. },270)*/
  315.  
  316. }
  317.  
  318. }
  319.  
  320.  
  321. let zCache=document.querySelector('[placeholder-for-youtube-play-next-queue] #items ytd-compact-video-renderer:last-of-type')
  322. if(cachedLastVideo && zCache && cachedLastVideo!==zCache){
  323.  
  324. // let cachedLastVideoStr = cachedLastVideo.__userscript_prev_textcontent__;
  325. // cachedLastVideo=zCache
  326. // cachedLastVideo.
  327. /* $0.textContent.replace(/\s+/g,'').replace(/[^\da-zA-Z\u4E00-\u9FFF]/g,'')*/
  328.  
  329.  
  330. const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  331.  
  332. if(!cachedLastVideo.parentNode/* || cachedLastVideoStr !== cachedLastVideo.*/){
  333. //removed
  334. fromSearch=true;
  335.  
  336. requestAnimationFrame(function(){
  337.  
  338. $('[placeholder-for-youtube-play-next-queue]')[0].scrollTop=0;
  339. const searchBox=document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  340. if(searchBox) searchBox.blur();
  341.  
  342. });
  343. }else if(searchBox && !zCache.__clone_last_results__){
  344.  
  345. let p=zCache.parentNode;
  346. //update from youtube loading
  347. setTimeout(function(){
  348.  
  349.  
  350. if(p&& p.parentNode) zCache.__clone_last_results__=$(p).clone();
  351.  
  352. //$()
  353. //$('ytd-watch-next-secondary-results-renderer')
  354.  
  355. },800)
  356.  
  357. }
  358. cachedLastVideo=zCache
  359.  
  360.  
  361. setTimeout(function(){
  362. const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  363. let zCache=document.querySelector('[placeholder-for-youtube-play-next-queue] ytd-watch-next-secondary-results-renderer #items>ytd-compact-video-renderer:last-of-type')
  364.  
  365. let items = document.querySelector('[placeholder-for-youtube-play-next-queue] ytd-watch-next-secondary-results-renderer #items');
  366. if(searchBox && $(searchBox).is(":visible") && (searchBox.value||"").length===0 && items.__clone_last_results__ && fromSearch ){
  367.  
  368.  
  369. if(items.__clone_last_results__){
  370.  
  371.  
  372. for(const s of items.querySelectorAll('ytd-compact-video-renderer')){
  373. try{
  374. items.removeChild(s);
  375. }catch(e){}
  376. }
  377. /*
  378. let texts=[];
  379.  
  380. for(const s of items.querySelectorAll('[placeholder-for-youtube-play-next-queue] ytd-item-section-renderer ytd-compact-video-renderer')){
  381. texts.push(extractTextContent(s))
  382. }*/
  383.  
  384. for(const s of items.__clone_last_results__.querySelectorAll('ytd-compact-video-renderer')){
  385. /* const text = extractTextContent(s)
  386. if(texts.indexOf(text)>0)continue;
  387. */ $(items).append(s)
  388. }
  389.  
  390. fromSearch=false;
  391.  
  392. }
  393.  
  394.  
  395. }
  396. },300)
  397.  
  398. }else if(!cachedLastVideo && zCache && cachedLastVideo!==zCache){
  399. cachedLastVideo=zCache
  400.  
  401. }
  402.  
  403.  
  404. }
  405.  
  406. function mtf_ChatExist(){
  407.  
  408. // no mutation triggering if the changes are inside the iframe
  409.  
  410. // 1) Detection of #continuations inside iframe
  411. // iframe ownerDocument is accessible due to same origin
  412. // if the chatroom is collasped, no determination of live chat or replay (as no #continuations and somehow a blank iframe doc)
  413.  
  414. // 2) Detection of meta tag
  415. // This is fastest but not reliable. It is somehow a bug that the navigation might not update the meta tag content
  416. // 3) Detection of HTMLElement inside video player for live video
  417. // (1)+(3) = solution
  418.  
  419. const elmChat = document.querySelector('ytd-live-chat-frame#chat')
  420. let elmCont = null;
  421. if(elmChat){
  422. elmCont=chatFrameElement('yt-live-chat-renderer #continuations')
  423. }
  424. const chatBlockR = (elmChat?1:0)+(elmCont?2:0)
  425. if(Q.mtf_chatBlockQ!==chatBlockR){
  426.  
  427. //console.log(897, Q.mtf_chatBlockQ, chatBlockR)
  428. Q.mtf_chatBlockQ=chatBlockR
  429.  
  430. const cssElm = document.querySelector('ytd-watch-flexy')
  431. if(elmChat){
  432.  
  433. let s=0;
  434. if(elmCont){
  435. //not found if it is collasped.
  436. s |= elmCont.querySelector('yt-timed-continuation')?1:0;
  437. s |= elmCont.querySelector('yt-live-chat-replay-continuation, yt-player-seek-continuation')?2:0;
  438. //s |= elmCont.querySelector('yt-live-chat-restricted-participation-renderer')?4:0;
  439. if(s==1) {
  440. cssElm.setAttribute('userscript-chatblock', 'chat-live')
  441. requestingComments=null;
  442. }
  443. if(s==2) cssElm.setAttribute('userscript-chatblock', 'chat-playback')
  444. //if(s==5) cssElm.setAttribute('userscript-chatblock', 'chat-live-paid')
  445.  
  446. if(s==1) $("span#tab3-txt-loader").text('');
  447.  
  448. }
  449. //keep unknown as original
  450. if( !cssElm.hasAttribute) cssElm.setAttribute('userscript-chatblock', '')
  451.  
  452. }else{
  453. cssElm.removeAttribute('userscript-chatblock')
  454. cssElm.removeAttribute('userscript-chat-collapsed')
  455.  
  456. }
  457.  
  458. }
  459. }
  460.  
  461.  
  462.  
  463.  
  464. let lastScrollAt = 0;
  465.  
  466. function makeBodyScrollByEvt(){
  467. // inside marco task (event)
  468.  
  469. Promise.resolve().then(()=>window.dispatchEvent(new Event("scroll")))
  470.  
  471.  
  472. }
  473.  
  474. function makeBodyScroll() {
  475.  
  476. // avoid over triggering scroll event
  477. if (+new Date - lastScrollAt < 30) return;
  478. lastScrollAt = +new Date;
  479.  
  480. //required for youtube content display
  481.  
  482. requestAnimationFrame(()=>{
  483.  
  484. window.dispatchEvent(new Event("scroll"));
  485.  
  486. })
  487.  
  488.  
  489. }
  490.  
  491. let requestingComments = null
  492. function scrollForComments_TF(){
  493. let comments = requestingComments;
  494. if ( comments && comments.hasAttribute('hidden')) makeBodyScroll();
  495. }
  496. function scrollForComments() {
  497. setTimeout(scrollForComments_TF, 80);
  498. setTimeout(scrollForComments_TF, 240);
  499. setTimeout(scrollForComments_TF, 870);
  500. }
  501.  
  502.  
  503.  
  504. let mtoNav = null;
  505.  
  506.  
  507. const mtoVs={}
  508.  
  509.  
  510. function initObserver(){
  511.  
  512.  
  513.  
  514.  
  515. // continuous check for element relocation
  516. function mtf_append_comments() {
  517. let comments = document.querySelector('#primary ytd-watch-metadata ~ #info ~ ytd-comments#comments');
  518. if (comments) $(comments).appendTo('#tab-comments').attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)
  519. }
  520.  
  521. // continuous check for element relocation
  522. function mtf_liveChatBtnF() {
  523. let button = document.querySelector('ytd-live-chat-frame#chat>.ytd-live-chat-frame#show-hide-button:nth-child(n+2)');
  524. if (button) button.parentNode.insertBefore(button, button.parentNode.firstChild)
  525. }
  526.  
  527.  
  528. // continuous check for element relocation
  529. // fired at begining & window resize, etc
  530. function mtf_append_playlist(){
  531. let ple1 = document.querySelector("*:not(#ytd-userscript-playlist)>ytd-playlist-panel-renderer#playlist");
  532. if(ple1){
  533. appendWithWrapper(
  534. ple1,
  535. 'ytd-userscript-playlist',
  536. document.querySelector("#tab-list")
  537. );
  538. $(ple1).attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)
  539.  
  540. }
  541. }
  542.  
  543.  
  544. // content fix - info & playlist
  545. // fired at begining, and keep for in case any change
  546. function mtf_fix_details() {
  547.  
  548. const content = document.querySelector('#meta-contents ytd-expander>#content, #tab-info ytd-expander>#content')
  549. if (content) {
  550. const expander = content.parentNode;
  551.  
  552. if (expander.hasAttribute('collapsed')) setAttr(expander,'collapsed',false);
  553.  
  554. let btn1 = expander.querySelector('tp-yt-paper-button#less:not([hidden])');
  555. let btn2 = expander.querySelector('tp-yt-paper-button#more:not([hidden])');
  556.  
  557. if (btn1) setAttr(btn1,'hidden',false);
  558. if (btn2) setAttr(btn2,'hidden',false);
  559.  
  560. }
  561.  
  562. // just in case the playlist is collapsed
  563. const playlist = document.querySelector('#tab-list ytd-playlist-panel-renderer#playlist')
  564. if(playlist){
  565. if(playlist.hasAttribute('collapsed')) setAttr(playlist,'collapsed',false);
  566. if(playlist.hasAttribute('collapsible')) setAttr(playlist,'collapsible',false);
  567. }
  568.  
  569.  
  570. }
  571.  
  572.  
  573.  
  574. let mtoNav_requestNo=0;
  575.  
  576. let mtoNav_delayedF = () => {
  577. let {addP, removeP} = Q;
  578. Q.addP = 0;
  579. Q.removeP = 0;
  580.  
  581.  
  582. let promisesForAddition=!scriptEnable?[]:addP > 0?[
  583. Q.$callOnceAsync('mtf_advancedComments'),
  584. Q.$callOnceAsync('mtf_infoSectionHeight'),
  585. Q.$callOnceAsync('mtf_checkDescriptionLoaded'),
  586. Q.$callOnceAsync('mtf_checkPlayList'),
  587. Q.$callOnceAsync('mtf_fetchCommentsAvailable'),
  588. Q.$callOnceAsync('mtf_initalAttr_comments'),
  589. Q.$callOnceAsync('mtf_initalAttr_playlist'),
  590. Q.$callOnceAsync('mtf_checkStatus_chatroom'),
  591. Q.$callOnceAsync('mtf_checkFlexy'),
  592. Q.$callOnceAsync('mtf_forceCheckLiveVideo'),
  593.  
  594. (async () => {
  595. mtf_append_comments();
  596. })(),
  597. (async () => {
  598. mtf_liveChatBtnF();
  599. })(),
  600.  
  601. (async ()=>{
  602. mtf_fixTabsAtTheEnd();
  603. })(),
  604. (async () => {
  605. mtf_append_playlist();
  606. })()
  607. ]:[];
  608.  
  609.  
  610. let promisesForEveryMutation=!scriptEnable?[]:[
  611. (async () => {
  612. mtf_fix_details();
  613. })(),
  614. (async () => {
  615. mtf_ChatExist();
  616. })()
  617. ];
  618.  
  619.  
  620. Promise.all([...promisesForAddition,...promisesForEveryMutation]).then(()=>{
  621. mtoNav_requestNo--;
  622. //console.log('motnav reduced to', mtoNav_requestNo)
  623. if(mtoNav_requestNo>0){
  624. mtoNav_requestNo=1;
  625. setTimeout(mtoNav_delayedF,mtoInterval);
  626. }
  627. })
  628. }
  629.  
  630. Q.addP=0;
  631. Q.removeP=0;
  632. let hReqNo=0;
  633. const mtoNavF=(mutations, observer) => {
  634.  
  635. let ch = false;
  636. for (const mutation of mutations) {
  637. for (const addedNode of mutation.addedNodes)
  638. if (addedNode.nodeType === 1) {
  639. Q.addP++
  640. ch = true;
  641. }
  642. for (const removedNode of mutation.removedNodes)
  643. if (removedNode.nodeType === 1) {
  644. Q.removeP++;
  645. ch = true;
  646. }
  647. }
  648. if (!ch) return;
  649.  
  650. mtoNav_requestNo++;
  651. hReqNo++;
  652. if(hReqNo==36) {
  653. mtoInterval=mtoInterval2;
  654. clickInterval=clickInterval2;
  655. }
  656. //console.log('motnav added to', mtoNav_requestNo)
  657.  
  658. if(mtoNav_requestNo==1) setTimeout(mtoNav_delayedF,mtoInterval);
  659.  
  660. }
  661. mtoNav = new MutationObserver(mtoNavF);
  662. mtoNav.observe(document.querySelector('ytd-watch-flexy'), {
  663. subtree: true,
  664. childList: true
  665. })
  666.  
  667. 1;1&&(async()=>{
  668. Q.addP=1; //fake the function
  669. mtoNav_requestNo++;
  670. if(mtoNav_requestNo==1) mtoNav_delayedF();
  671.  
  672. })();
  673. }
  674.  
  675. let displayedPlaylist=null
  676. let scrollingVideosList=null
  677.  
  678. let scriptEnable =false;
  679. let lastShowTab = null;
  680.  
  681.  
  682. let cachedLastVideo=null;
  683. let fromSearch=false;
  684. function resetBeforeNav() {
  685. fromSearch=true;
  686. cachedLastVideo=null;
  687. lastShowTab=null;
  688. displayedPlaylist=null
  689. scrollingVideosList=null
  690. scriptEnable =false;
  691.  
  692.  
  693. clearMutationObserver(mtoVs,'mtoVisibility_Playlist')
  694. clearMutationObserver(mtoVs,'mtoVisibility_Comments')
  695. clearMutationObserver(mtoVs,'mtoVisibility_Chatroom')
  696. clearMutationObserver(mtoVs,'mtoFlexyAttr')
  697.  
  698. if (mtoNav) {
  699. mtoNav.takeRecords();
  700. mtoNav.disconnect();
  701. mtoNav = null;
  702.  
  703. Q.mtf_advancedComments=null;
  704. Q.mtf_checkDescriptionLoaded = null;
  705. Q.mtf_checkPlayList = null;
  706. Q.mtf_fetchCommentsAvailable = null;
  707. Q.mtf_initalAttr_comments = null;
  708. Q.mtf_initalAttr_playlist = null;
  709. Q.mtf_checkStatus_chatroom = null;
  710. Q.mtf_forceCheckLiveVideo=null;
  711. Q.mtf_chatBlockQ = null;
  712. }
  713.  
  714. mtoInterval = mtoInterval1;
  715. clickInterval = clickInterval1;
  716.  
  717. }
  718.  
  719. function resetAtNav() {
  720.  
  721. scriptEnable =true;
  722.  
  723. $("ytd-watch-flexy").removeAttr("userscript-chatblock").removeAttr("userscript-chat-collapsed");
  724. $('#tab-comments').attr('lazy-loading', '');
  725. $('span#tab3-txt-loader').text('');
  726.  
  727. //removed any cache of #comments header (i.e. count message)
  728. var prevCommentsHeader = document.querySelector('ytd-comments#comments ytd-comments-header-renderer');
  729. if (prevCommentsHeader) prevCommentsHeader.parentNode.removeChild(prevCommentsHeader);
  730.  
  731. var prevCommentsMsg= document.querySelector('ytd-item-section-renderer#sections #header ~ #contents>ytd-message-renderer:only-child');
  732. if (prevCommentsMsg) prevCommentsMsg.parentNode.removeChild(prevCommentsMsg);
  733.  
  734. //force to [hidden]
  735. var prevComemnts = document.querySelector('ytd-comments#comments');
  736. if (prevComemnts) {
  737. setAttr(prevComemnts, 'hidden', true);
  738. requestingComments = prevComemnts;
  739. //scrollForComments();
  740. }
  741.  
  742. //playlist bug
  743. /*
  744. var prevPlaylist = document.querySelector('ytd-watch-flexy #columns ytd-playlist-panel-renderer#playlist')
  745. var secondInner = document.querySelector('ytd-watch-flexy #secondary>#secondary-inner');
  746. if (prevPlaylist && secondInner){
  747.  
  748.  
  749. prevPlaylist.removeAttribute('hidden')
  750. secondInner.appendChild(prevPlaylist)
  751.  
  752. }
  753.  
  754. var prevRelated = document.querySelector('ytd-watch-flexy #columns #related')
  755. if (prevRelated && secondInner){
  756.  
  757. secondInner.appendChild(prevRelated)
  758. }*/
  759.  
  760.  
  761. }
  762.  
  763. function getTabsHTML(){
  764.  
  765.  
  766. let ts = settings.toggleSettings;
  767.  
  768. if (!ts.tabs) return;
  769.  
  770. const sTabBtnVideos = `${svgElm(16,16,298,298,svgVideos)}<span>Videos</span>`
  771. const sTabBtnInfo = `${svgElm(16,16,23.625,23.625,svgInfo)}<span>Info</span>`
  772. const sTabBtnPlayList = `${svgElm(16,16,426.667,426.667,svgPlayList)}<span>Playlist</span>`
  773.  
  774. const str1 = `
  775. <paper-ripple class="style-scope yt-icon-button">
  776. <div id="background" class="style-scope paper-ripple" style="opacity:0;"></div>
  777. <div id="waves" class="style-scope paper-ripple"></div>
  778. </paper-ripple>
  779. `;
  780.  
  781. const str_tabs = [
  782. ts.tInfo ? `<a id="tab-btn1" data-name="info" userscript-tab-content="#tab-info" class="tab-btn">${sTabBtnInfo}${str1}</a>` : '',
  783. `<a id="tab-btn2" userscript-tab-content="#tab-live" class="tab-btn tab-btn-hidden">Chat${str1}</a>`,
  784. 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>` : '',
  785. ts.tVideos ? `<a id="tab-btn4" userscript-tab-content="#tab-videos" data-name="videos" class="active tab-btn">${sTabBtnVideos}${str1}</a>` : '',
  786. `<a id="tab-btn5" userscript-tab-content="#tab-list" class="tab-btn">${sTabBtnPlayList}${str1}</a>`
  787. ].join('')
  788.  
  789. var addHTML = `
  790. <div id="right-tabs">
  791. <header>
  792. <div id="material-tabs">
  793. ${str_tabs}
  794. </div>
  795. </header>
  796. <div class="tab-content">
  797. <div id="tab-info" class="tab-content-cld" userscript-scrollbar-render></div>
  798. <div id="tab-live" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  799. <div id="tab-comments" class="tab-content-cld" userscript-scrollbar-render></div>
  800. <div id="tab-videos" class="tab-content-cld" userscript-scrollbar-render></div>
  801. <div id="tab-list" class="tab-content-cld" userscript-scrollbar-render></div>
  802. </div>
  803. </div>
  804. `;
  805.  
  806. return addHTML
  807.  
  808. }
  809.  
  810. function onNavigationEnd() {
  811.  
  812. resetBeforeNav();
  813. if(!/https?\:\/\/(\w+\.)*youtube\.com\/watch\?(\w+\=[^\/\?\&]+\&)*v=[\w\-\_]+/.test(window.location.href))return;
  814. resetAtNav();
  815.  
  816.  
  817. let promise = Promise.resolve();
  818.  
  819. if (!document.querySelector("#right-tabs")) {
  820. let targetElm = document.querySelector("ytd-watch-flexy #secondary>#secondary-inner")||document.querySelector("ytd-watch-flexy #secondary")||document.querySelector("ytd-watch-flexy #columns");
  821. if(!targetElm) throw 'Userscript: Two Column flexy layout not found'; // not flexy layout
  822. promise=promise.then(()=>{
  823. $(getTabsHTML()).appendTo(targetElm).attr('data-dom-created-by-tabview-youtube',scriptVersionForExternal);
  824. targetElm=null;
  825. })
  826. }
  827.  
  828. promise.then(runAfterTabAppended).then(initObserver)
  829.  
  830. }
  831.  
  832. function setToActiveTab() {
  833. if(isTheater() && isWideScreenWithTwoColumns())return;
  834. const jElm = document.querySelector(`a[userscript-tab-content="${switchTabActivity_lastTab}"]:not(.tab-btn-hidden)`) ||
  835. document.querySelector(`a[userscript-tab-content="#tab-${settings.defaultTab}"]:not(.tab-btn-hidden)`) ||
  836. document.querySelector("a[userscript-tab-content]:not(.tab-btn-hidden)") ||
  837. null;
  838. switchTabActivity(jElm);
  839. }
  840.  
  841. function insertBefore(elm, p) {
  842. if (elm && p && p.parentNode)
  843. p.parentNode.insertBefore(elm, p);
  844. }
  845.  
  846. function appendWithWrapper(elm, wrapperId, toParent){
  847. if(!toParent||!elm)return;
  848. let $wrapper = $(`#${wrapperId}`);
  849. if(!$wrapper[0]) $wrapper=$(`<div id="${wrapperId}"></div>`)
  850. $wrapper.append(elm).appendTo(toParent);
  851. }
  852.  
  853. function runAfterTabAppended() {
  854.  
  855. // just switch to the default tab
  856. setToActiveTab();
  857.  
  858. // append the next videos
  859. // it exists as "related" is already here
  860.  
  861. fixRelated();
  862.  
  863.  
  864.  
  865. prepareTabBtn();
  866.  
  867. // append the detailed meta contents to the tab-info
  868. Q.mtf_checkDescriptionLoaded = () => {
  869. const expander = document.querySelector("#meta-contents ytd-expander");
  870. if (!expander) return true;
  871. $(expander).appendTo("#tab-info").attr('data-dom-changed-by-tabview-youtube',scriptVersionForExternal)
  872.  
  873. const avatar = document.querySelector('ytd-watch-flexy #meta-contents yt-img-shadow#avatar');
  874. if(avatar) hackImgShadow(avatar)
  875. return false;
  876. }
  877. Q.$callOnceAsync('mtf_checkDescriptionLoaded')
  878.  
  879. // force window scroll when #continuations is first detected and #comments still [hidden]
  880. Q.mtf_advancedComments = () => {
  881. const continuations = document.querySelector("ytd-comments#comments #continuations");
  882. if (!continuations) return true;
  883. requestingComments = document.querySelector('ytd-comments#comments');
  884. scrollForComments();
  885. return false;
  886. }
  887. Q.$callOnceAsync('mtf_advancedComments')
  888.  
  889. /*
  890. Q.mtf_infoSectionHeight=()=>{
  891. const infoSection = document.querySelector("#primary #player ~ #info>#info-contents");
  892. if (!infoSection) return true;
  893. if(mtoVs.rsoInfoSection) {
  894. mtoVs.rsoInfoSection.disconnect();
  895. mtoVs.rsoInfoSection=null;
  896. }
  897. mtoVs.rsoInfoSection=new ResizeObserver(()=>{
  898. const cssElm = document.querySelector('ytd-watch-flexy')
  899. if(!cssElm)return;
  900. cssElm.style.setProperty('--userscript-info-section-height', infoSection.offsetHeight);
  901. });
  902. return false;
  903. }
  904. Q.$callOnceAsync('mtf_infoSectionHeight')*/
  905.  
  906.  
  907.  
  908. // make window scroll event from playlist scrolling
  909. // i guess it shall be not neccessary, just in case
  910. /*Q.mtf_checkPlayList = () => {
  911. const items= document.querySelector('ytd-playlist-panel-renderer>#container>#items');
  912. if(!items) return true;
  913. $(items).scroll(makeBodyScrollByEvt);
  914. return false;
  915. }
  916. Q.$callOnceAsync('mtf_checkPlayList')*/
  917.  
  918.  
  919. // use video player's element to detect the live-chat situation (no commenting section)
  920. // 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
  921. Q.mtf_forceCheckLiveVideo_tf =()=>{
  922. const cssElm = document.querySelector('ytd-watch-flexy')
  923. if(!cssElm) return;
  924. if($('#ytd-player .ytp-time-display').is('.ytp-live')) {
  925. cssElm.setAttribute('userscript-chatblock', 'chat-live')
  926. requestingComments=null;
  927. }
  928. }
  929. Q.mtf_forceCheckLiveVideo = () => {
  930. const playerLabel = document.querySelector('#ytd-player .ytp-time-display') && document.querySelector('ytd-live-chat-frame#chat')
  931. if (!playerLabel) return true;
  932. setTimeout(Q.mtf_forceCheckLiveVideo_tf,170)
  933. return false;
  934. }
  935. Q.$callOnceAsync('mtf_forceCheckLiveVideo')
  936.  
  937.  
  938.  
  939. createAttributeObservants();
  940. checkChatStatus();
  941.  
  942.  
  943. $("#right-tabs [userscript-scrollbar-render]").scroll(makeBodyScrollByEvt);
  944.  
  945. }
  946.  
  947.  
  948. async function asyncFetchCommentsAvailable() {
  949.  
  950. let span = document.querySelector("span#tab3-txt-loader")
  951. if (!span) return;
  952.  
  953. makeBodyScroll();
  954.  
  955. let fetchedOnce = false
  956. Q.mtf_fetchCommentsAvailable = () => {
  957.  
  958. if(!scriptEnable)return;
  959.  
  960. let messageElm, messageStr;
  961. const commentRenderer = document.querySelector("ytd-comments#comments #count.ytd-comments-header-renderer");
  962. if (commentRenderer) {
  963. fetchedOnce=true;
  964. let r = '0';
  965. let txt = commentRenderer.textContent
  966. if (typeof txt == 'string') {
  967. let m = txt.match(/[\d\,\s]+/)
  968. if (m) r = m[0].trim()
  969. }
  970. span.textContent = r;
  971. $('#tab-comments[lazy-loading]').removeAttr('lazy-loading')
  972. mtoInterval=mtoInterval2;
  973. clickInterval=clickInterval2;
  974. }else if((messageElm = document.querySelector('ytd-item-section-renderer#sections #header ~ #contents>ytd-message-renderer:only-child'))&&(messageStr=(messageElm.textContent||'').trim())){ //ytd-message-renderer
  975. // it is possible to get the message before the header generation.
  976. setTimeout(function(){
  977. if(fetchedOnce)return;
  978. const mainMsg= messageElm.querySelector('#message, #submessage')
  979. if(mainMsg && mainMsg.textContent){
  980. for(const msg of mainMsg.querySelectorAll('*:not(:empty)')){
  981. if(msg.childElementCount===0 && msg.textContent) {
  982. messageStr=msg.textContent.trim()
  983. break
  984. }
  985. }
  986. }
  987. span.textContent = messageStr;
  988. $('#tab-comments[lazy-loading]').removeAttr('lazy-loading')
  989. },240);
  990. }
  991. return true;
  992. }
  993. Q.$callOnceAsync('mtf_fetchCommentsAvailable')
  994.  
  995.  
  996. }
  997.  
  998.  
  999.  
  1000.  
  1001. function createAttributeObservants() {
  1002.  
  1003.  
  1004. // Attr Mutation Observer - #playlist - hidden
  1005. clearMutationObserver(mtoVs,'mtoVisibility_Playlist')
  1006. // Attr Mutation Observer callback - #playlist - hidden
  1007. let mtf_attrPlaylist=(mutations, observer)=>{
  1008. var playlist=document.querySelector('ytd-playlist-panel-renderer#playlist')
  1009. const $tabBtn = $('[userscript-tab-content="#tab-list"]');
  1010. //console.log(3712,$tabBtn)
  1011. //console.log('attr playlist changed')
  1012. if( $tabBtn.is('.tab-btn-hidden') && !playlist.hasAttribute('hidden') ){
  1013. //console.log(3713)
  1014. //console.log('attr playlist changed - no hide')
  1015. $tabBtn.removeClass("tab-btn-hidden");
  1016. }else if( !$tabBtn.is('.tab-btn-hidden') && playlist.hasAttribute('hidden') ){
  1017. //console.log(3714)
  1018. //console.log('attr playlist changed - add hide')
  1019. hideTabBtn($tabBtn);
  1020. }
  1021. }
  1022.  
  1023. // pending for #playlist and set Attribute Observer
  1024. Q.mtf_initalAttr_playlist=()=>{
  1025. var playlist=document.querySelector('ytd-playlist-panel-renderer#playlist')
  1026. if(!playlist) return true;
  1027. initMutationObserver(mtoVs,'mtoVisibility_Playlist', mtf_attrPlaylist)
  1028. mtoVs.mtoVisibility_Playlist.observe(playlist, {
  1029. attributes: true,
  1030. attributeFilter: ['hidden'],
  1031. attributeOldValue: true
  1032. })
  1033. //console.log(3711)
  1034. mtf_attrPlaylist()
  1035. return false;
  1036. }
  1037. //console.log(3710)
  1038. Q.$callOnceAsync('mtf_initalAttr_playlist')
  1039.  
  1040.  
  1041.  
  1042.  
  1043.  
  1044. // Attr Mutation Observer - ytd-comments#comments - hidden
  1045. clearMutationObserver(mtoVs,'mtoVisibility_Comments')
  1046. // Attr Mutation Observer callback - ytd-comments#comments - hidden
  1047. let mtf_attrComments=(mutations, observer)=>{
  1048. var comments=document.querySelector('ytd-comments#comments')
  1049. const $tabBtn = $('[userscript-tab-content="#tab-comments"]');
  1050. if(!comments || !$tabBtn[0])return;
  1051. //console.log('attr comments changed')
  1052. if( $tabBtn.is('.tab-btn-hidden') && !comments.hasAttribute('hidden') ){
  1053. //console.log('attr comments changed - no hide')
  1054. $tabBtn.removeClass("tab-btn-hidden");
  1055. asyncFetchCommentsAvailable();
  1056. }else if( !$tabBtn.is('.tab-btn-hidden') && comments.hasAttribute('hidden') ){
  1057. //console.log('attr comments changed - add hide')
  1058. if(!document.querySelector('[userscript-chatblock="chat-live"]')){
  1059. requestingComments=comments
  1060. $('#tab-comments').attr('lazy-loading','')
  1061. }
  1062. $('span#tab3-txt-loader').text('');
  1063. hideTabBtn($tabBtn);
  1064. }
  1065. }
  1066.  
  1067. // pending for #comments and set Attribute Observer
  1068. Q.mtf_initalAttr_comments=()=>{
  1069. var comments=document.querySelector('ytd-comments#comments')
  1070. if(!comments) return true;
  1071. initMutationObserver(mtoVs,'mtoVisibility_Comments',mtf_attrComments)
  1072. mtoVs.mtoVisibility_Comments.observe(comments, {
  1073. attributes: true,
  1074. attributeFilter: ['hidden'],
  1075. attributeOldValue: true
  1076. })
  1077. mtf_attrComments()
  1078. requestingComments = document.querySelector('ytd-comments#comments');
  1079. scrollForComments()
  1080. return false;
  1081. }
  1082. Q.$callOnceAsync('mtf_initalAttr_comments')
  1083.  
  1084. }
  1085.  
  1086.  
  1087. function isEmptyBody(){
  1088. //only deal with loaded document without body
  1089. //other situation, case by case
  1090.  
  1091. let iframe = document.querySelector('ytd-live-chat-frame iframe#chatframe');
  1092.  
  1093. if(!iframe) return false; //iframe must be there
  1094.  
  1095. if(iframe.readyState != 'complete') return false; //we must wait for its completion
  1096.  
  1097. let doc = null;
  1098. try{
  1099.  
  1100. doc=iframe.contentDocument
  1101.  
  1102. }catch(e){}
  1103.  
  1104. if(!doc) return false; //might be not loaded yet
  1105.  
  1106. if(doc.body && doc.body.childElementCount===0){
  1107. //empty body
  1108.  
  1109. return true;
  1110. }
  1111.  
  1112.  
  1113. }
  1114.  
  1115. function checkChatStatus(){
  1116. clearMutationObserver(mtoVs,'mtoVisibility_Chatroom')
  1117.  
  1118. let cid_chatFrameCheck=0;
  1119.  
  1120. let mtf_attrChatroom=(mutations, observer)=>{
  1121.  
  1122. const chatBlock = document.querySelector('ytd-live-chat-frame#chat')
  1123. const cssElm = document.querySelector('ytd-watch-flexy')
  1124. if(!cssElm.hasAttribute('userscript-chatblock')) setAttr(cssElm, 'userscript-chatblock', true);
  1125. setAttr(cssElm,'userscript-chat-collapsed',!!chatBlock.hasAttribute('collapsed'));
  1126.  
  1127. if(cssElm.hasAttribute('userscript-chatblock')&&!chatBlock.hasAttribute('collapsed')) lastShowTab='#chatroom'
  1128.  
  1129. if( chatBlock && cssElm && cssElm.hasAttribute('userscript-chatblock') && !chatBlock.hasAttribute('collapsed') && !cid_chatFrameCheck){
  1130. let dd=+new Date;
  1131. cid_chatFrameCheck=setInterval(()=>{
  1132. // mutation on iframe window would not trigger the observer
  1133. // just check the first few seconds for this purpose.
  1134. let chatFrameChecking, iframe;
  1135. if(+new Date - dd>6750){
  1136. //
  1137. }else if( isEmptyBody() ){
  1138.  
  1139. // bug. youtube iframe loaded with nothing
  1140.  
  1141. let button = document.querySelector('ytd-live-chat-frame#chat>.ytd-live-chat-frame#show-hide-button ytd-toggle-button-renderer')
  1142. if (button) {
  1143. setTimeout(function(){
  1144. if(button && button.parentNode && isChatExpand()){
  1145. button.click();
  1146. setTimeout(function(){
  1147. if(button && button.parentNode && !isChatExpand()) button.click();
  1148. button=null;
  1149. },80)
  1150. }else{
  1151. button=null;
  1152. }
  1153. },20)
  1154. }
  1155.  
  1156. }else if(chatFrameChecking=!!chatFrameElement('yt-live-chat-renderer #continuations')){
  1157. mtf_ChatExist();
  1158. $(document.querySelector('ytd-live-chat-frame#chat')).attr('yt-userscript-iframe-loaded','')
  1159. }else{
  1160. return;
  1161. }
  1162. return (cid_chatFrameCheck=clearInterval(cid_chatFrameCheck));
  1163. },270)
  1164. }else if(chatBlock){
  1165. chatBlock.removeAttribute('yt-userscript-iframe-loaded')
  1166.  
  1167. }
  1168.  
  1169.  
  1170. }
  1171.  
  1172. Q.mtf_checkStatus_chatroom=()=>{
  1173. var chatroom=document.querySelector('ytd-live-chat-frame#chat')
  1174. if(!chatroom) return true;
  1175. initMutationObserver(mtoVs,'mtoVisibility_Chatroom',mtf_attrChatroom)
  1176. mtoVs.mtoVisibility_Chatroom.observe(chatroom, {
  1177. attributes: true,
  1178. attributeFilter: ['collapsed'],
  1179. attributeOldValue: true
  1180. })
  1181. mtf_attrChatroom()
  1182. return false;
  1183. }
  1184. Q.$callOnceAsync('mtf_checkStatus_chatroom')
  1185.  
  1186.  
  1187.  
  1188.  
  1189.  
  1190. clearMutationObserver(mtoVs,'mtoFlexyAttr')
  1191.  
  1192.  
  1193. let mtf_attrFlexy=(mutations, observer)=>{
  1194.  
  1195.  
  1196. const cssElm=document.querySelector('ytd-watch-flexy');
  1197. if(!cssElm)return;
  1198.  
  1199. let chatBlockStatusChanged = false
  1200. let theaterStatusChanged = false;
  1201. let twoColStatusChanged = false;
  1202. let initalTriggering = !mutations
  1203.  
  1204. if(!initalTriggering){
  1205. for(const mutation of mutations) {
  1206. if (mutation.attributeName == 'theater') {
  1207. theaterStatusChanged=true;
  1208. }
  1209. if (mutation.attributeName == 'userscript-chat-collapsed' || 'userscript-chatblock'){
  1210. chatBlockStatusChanged=true
  1211. }
  1212. if (mutation.attributeName == 'is-two-columns_'){
  1213. twoColStatusChanged=true;
  1214. }
  1215. }
  1216. }
  1217.  
  1218. if(theaterStatusChanged || chatBlockStatusChanged || twoColStatusChanged || initalTriggering ){
  1219.  
  1220.  
  1221. if(twoColStatusChanged || initalTriggering){
  1222.  
  1223. fixDisplayForTheaterModeChanged();
  1224.  
  1225. }else if(theaterStatusChanged){
  1226.  
  1227. isChatExpandBeforeTheaterChange = isChatExpand()
  1228. requestAnimationFrame(fixDisplayForTheaterModeChanged)
  1229. }else if(chatBlockStatusChanged){
  1230. //chatroom is shown or hidden
  1231.  
  1232. let isOpenChatFrame = !cssElm.hasAttribute('userscript-chat-collapsed') && cssElm.hasAttribute('userscript-chatblock')
  1233.  
  1234. new Promise(requestAnimationFrame).then(() => {
  1235. if (isOpenChatFrame && !isTheater() && isWideScreenWithTwoColumns()) {
  1236. switchTabActivity(null)
  1237. } else if(!isOpenChatFrame && !isTheater() && isWideScreenWithTwoColumns()){
  1238. setToActiveTab();
  1239. } else if(isTheater() && isWideScreenWithTwoColumns() && isOpenChatFrame && isWideScreenWithTwoColumns()){
  1240. ytBtnCancelTheater();
  1241. }
  1242. })
  1243.  
  1244. }
  1245.  
  1246.  
  1247. }
  1248.  
  1249.  
  1250.  
  1251.  
  1252.  
  1253.  
  1254. }
  1255.  
  1256.  
  1257. Q.mtf_checkFlexy=()=>{
  1258. var flexy=document.querySelector('ytd-watch-flexy')
  1259. if(!flexy) return true;
  1260. initMutationObserver(mtoVs,'mtoFlexyAttr',mtf_attrFlexy)
  1261. mtoVs.mtoFlexyAttr.observe(flexy, {
  1262. attributes: true,
  1263. attributeFilter: ['userscript-chat-collapsed','userscript-chatblock','theater','is-two-columns_'],
  1264. attributeOldValue: true
  1265. })
  1266. mtf_attrFlexy()
  1267.  
  1268.  
  1269. let columns = document.querySelector('ytd-page-manager#page-manager #columns')
  1270. if(columns){
  1271. setAttr(columns, 'userscript-scrollbar-render', true);
  1272. }
  1273.  
  1274. return false;
  1275. }
  1276. Q.$callOnceAsync('mtf_checkFlexy')
  1277.  
  1278.  
  1279.  
  1280. }
  1281.  
  1282.  
  1283.  
  1284.  
  1285. let switchTabActivity_lastTab = null
  1286.  
  1287. function switchTabActivity(activeLink) {
  1288.  
  1289.  
  1290. if (activeLink && $(activeLink).is('.tab-btn-hidden')) return; // not allow to switch to hide tab
  1291.  
  1292. if(isTheater() && isWideScreenWithTwoColumns()) activeLink=null;
  1293.  
  1294. const links = document.querySelectorAll('#material-tabs a[userscript-tab-content]');
  1295.  
  1296.  
  1297.  
  1298. function runAtEnd(){
  1299.  
  1300. if(activeLink) lastShowTab=activeLink.getAttribute('userscript-tab-content')
  1301.  
  1302.  
  1303. //override the default youtube coding event prevention
  1304.  
  1305. //let elm=$('ytd-watch-flexy:not([is-two-columns_]) #tab-list:not(.tab-content-hidden) ytd-playlist-panel-renderer')[0];
  1306. let elm=$('ytd-watch-flexy #tab-list:not(.tab-content-hidden) ytd-playlist-panel-renderer')[0];
  1307. displayedPlaylist=elm;
  1308. if(!!displayedPlaylist) $('ytd-watch-flexy').attr('userscript-auto-scroll-playlist',''); else $('ytd-watch-flexy').removeAttr('userscript-auto-scroll-playlist');
  1309. scrollingVideosList=$('ytd-watch-flexy #tab-videos:not(.tab-content-hidden) [placeholder-videos]')[0]
  1310. }
  1311.  
  1312. for (const link of links) {
  1313. let content = document.querySelector(link.getAttribute('userscript-tab-content'));
  1314. if (link && content) {
  1315. if (link !== activeLink) {
  1316. $(link).removeClass("active");
  1317. $(content).addClass("tab-content-hidden");
  1318. } else {
  1319. $(link).addClass("active");
  1320. $(content).removeClass("tab-content-hidden");
  1321. window.requestAnimationFrame(() => {
  1322. content.focus()
  1323.  
  1324. runAtEnd()
  1325. })
  1326. }
  1327. }
  1328. }
  1329.  
  1330. if(!activeLink){
  1331. runAtEnd();
  1332. }
  1333.  
  1334.  
  1335. }
  1336.  
  1337. let tabsUiScript_setclick = false;
  1338.  
  1339. function prepareTabBtn() {
  1340.  
  1341. const materialTab = document.querySelector("#material-tabs")
  1342. if (!materialTab) return;
  1343.  
  1344. let noActiveTab = !!document.querySelector('ytd-watch-flexy[userscript-chatblock]:not([userscript-chat-collapsed])')
  1345.  
  1346. const activeLink = materialTab.querySelector('a[userscript-tab-content].active:not(.tab-btn-hidden)')
  1347. if (activeLink) switchTabActivity(noActiveTab ? null : activeLink)
  1348.  
  1349. if (!tabsUiScript_setclick) {
  1350. tabsUiScript_setclick = true;
  1351. $(materialTab).on("click", "a", function(evt) {
  1352.  
  1353. if (!this.hasAttribute('userscript-tab-content')) return;
  1354.  
  1355. switchTabActivity_lastTab = this.getAttribute('userscript-tab-content');
  1356.  
  1357. if( isWideScreenWithTwoColumns() && !isTheater() && $(this).is(".tab-btn.active:not(.tab-btn-hidden)")){
  1358.  
  1359. const sizeBtn=document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
  1360. if(sizeBtn) sizeBtn.click();
  1361.  
  1362. }else if($(this).is(".tab-btn.active:not(.tab-btn-hidden)")){
  1363.  
  1364. switchTabActivity(null);
  1365.  
  1366.  
  1367. /*
  1368. setTimeout(()=>{
  1369. window.scrollTo(0, 0);
  1370. },60)*/
  1371.  
  1372.  
  1373.  
  1374. }else{
  1375.  
  1376. new Promise(requestAnimationFrame).then(() => {
  1377. if(isChatExpand() && isWideScreenWithTwoColumns()) ytBtnCollapseChat();
  1378. else if(isWideScreenWithTwoColumns() && isTheater() ) ytBtnCancelTheater();
  1379. }).then(() => {
  1380. setTimeout(()=>{
  1381. switchTabActivity(this)
  1382. //setTimeout(makeBodyScroll,20);
  1383.  
  1384.  
  1385. setTimeout(()=>{
  1386. let rightTabs=document.querySelector('#right-tabs');
  1387. if(!isWideScreenWithTwoColumns() && rightTabs && rightTabs.offsetTop>0 && $(this).is('.active')){
  1388.  
  1389. window.scrollTo(0, rightTabs.offsetTop);
  1390. }
  1391. },60)
  1392.  
  1393. }, clickInterval);
  1394. })
  1395.  
  1396. }
  1397.  
  1398.  
  1399. evt.preventDefault();
  1400. });
  1401.  
  1402. }
  1403.  
  1404. }
  1405.  
  1406.  
  1407. // ---------------------------------------------------------------------------------------------
  1408. window.addEventListener("yt-navigate-finish", onNavigationEnd)
  1409.  
  1410. const singleColumnScrolling = (function() {
  1411. var lastD = 0,
  1412. lastF = 0;
  1413.  
  1414. return function() {
  1415. let pageY = pageYOffset;
  1416. if (pageY < 10 && lastD === 0 && !lastF) return;
  1417.  
  1418. let targetElm, header, navElm;
  1419.  
  1420. Promise.resolve().then(() => {
  1421.  
  1422. targetElm = document.querySelector("#right-tabs");
  1423. if (!targetElm) return;
  1424. header = targetElm.querySelector("header");
  1425. if (!header) return;
  1426. navElm = document.querySelector('#masthead-container, #masthead')
  1427. if (!navElm) return;
  1428. navHeight = navElm ? navElm.offsetHeight : 0
  1429.  
  1430. let elmY = targetElm.offsetTop
  1431.  
  1432. let xyz = [elmY + navHeight, pageY, elmY - navHeight]
  1433.  
  1434. let xyStatus = 0
  1435. if (xyz[1] < xyz[2] && xyz[2] < xyz[0]) {
  1436. // 1
  1437. xyStatus = 1
  1438. }
  1439.  
  1440. if (xyz[0] > xyz[1] && xyz[1] > xyz[2]) {
  1441.  
  1442. //2
  1443. xyStatus = 2
  1444.  
  1445. }
  1446.  
  1447. if (xyz[2] < xyz[0] && xyz[0] < xyz[1]) {
  1448. // 3
  1449.  
  1450. xyStatus = 3
  1451.  
  1452.  
  1453. }
  1454.  
  1455. return xyStatus;
  1456.  
  1457. }).then((xyStatus) => {
  1458.  
  1459. if ((xyStatus == 2 || xyStatus == 3) && (lastD === 0 || lastF)) {
  1460. lastD = 1;
  1461. let {
  1462. offsetHeight
  1463. } = header
  1464. let {
  1465. offsetWidth
  1466. } = targetElm
  1467.  
  1468. targetElm.style.setProperty("--userscript-sticky-width", offsetWidth + 'px')
  1469. targetElm.style.setProperty("--userscript-sticky", offsetHeight + 'px')
  1470. setAttr(targetElm, 'userscript-sticky', true);
  1471.  
  1472. } else if ((xyStatus == 1) && (lastD === 1 || lastF)) {
  1473. lastD = 0;
  1474.  
  1475. setAttr(targetElm, 'userscript-sticky', false);
  1476. }
  1477.  
  1478.  
  1479. targetElm = null;
  1480. header = null;
  1481. navElm = null;
  1482.  
  1483. })
  1484.  
  1485. }
  1486. })();
  1487.  
  1488. window.addEventListener("scroll", function() {
  1489. if(!scriptEnable)return;
  1490. singleColumnScrolling()
  1491. }, {
  1492. capture: false,
  1493. passive: true
  1494. })
  1495.  
  1496. var lastTheatreStatus = 0
  1497. window.addEventListener('resize', function() {
  1498. if(!scriptEnable)return;
  1499.  
  1500. requestAnimationFrame(() => {
  1501. lastF = 1;
  1502. singleColumnScrolling()
  1503. lastF = 0;
  1504. })
  1505.  
  1506. }, {
  1507. capture: false,
  1508. passive: true
  1509. })
  1510.  
  1511. window.addEventListener('beforeunload', function() {
  1512. if(!scriptEnable)return;
  1513. console.log('beforeunload')
  1514. resetBeforeNav();
  1515. let video=document.querySelector('video');
  1516. if(video && !video.paused) video.pause();
  1517. }, {capture: true})
  1518.  
  1519. window.addEventListener('hashchange', function() {
  1520. if(!scriptEnable)return;
  1521. console.log('hashchange')
  1522. resetBeforeNav();
  1523. }, {capture: true})
  1524. window.addEventListener('popstate', function() {
  1525. if(!scriptEnable)return;
  1526. console.log('popstate')
  1527. resetBeforeNav();
  1528. }, {capture: true})
  1529.  
  1530.  
  1531. function clearMutationObserver(o, key){
  1532. if(o[key]) {
  1533. o[key].takeRecords();
  1534. o[key].disconnect();
  1535. o[key]=null;
  1536. }
  1537. }
  1538.  
  1539. function initMutationObserver(o, key, callback){
  1540.  
  1541.  
  1542. clearMutationObserver(o,key)
  1543. o[key]=new MutationObserver(callback)
  1544.  
  1545.  
  1546.  
  1547. }
  1548. /*
  1549.  
  1550. async function resizer(){
  1551.  
  1552. let full=true;
  1553. (async ()=>{
  1554. const primaryPlayer=$('ytd-watch-flexy #primary-inner>#player')[0]
  1555. cssElm.style.setProperty('--userscript-resizing-primary-player-height', primaryPlayer?primaryPlayer.offsetHeight:'')
  1556. if(!primaryPlayer)full=false;
  1557. })();
  1558.  
  1559.  
  1560. (async ()=>{
  1561. const primaryInner=$('ytd-watch-flexy #primary-inner')[0]
  1562. cssElm.style.setProperty('--userscript-resizing-primary-inner-height', primaryInner?primaryInner.offsetHeight:'')
  1563. if(!primaryInner)full=false;
  1564. })();
  1565. (async ()=>{
  1566. const infoContents=$('ytd-watch-flexy #primary-inner>#info>#info-contents')[0]
  1567. cssElm.style.setProperty('--userscript-resizing-primary-info-height', infoContents?infoContents.offsetHeight:'')
  1568. if(!infoContents)full=false;
  1569. })();
  1570.  
  1571. (async ()=>{
  1572. const metaContents=$('ytd-watch-flexy #primary-inner>#meta>#meta-contents')[0]
  1573. cssElm.style.setProperty('--userscript-resizing-primary-meta-height', metaContents?metaContents.offsetHeight:'')
  1574. if(!metaContents)full=false;
  1575. })();
  1576.  
  1577. (async ()=>{
  1578. const secondaryInner=$('ytd-watch-flexy #secondary-inner')[0]
  1579. cssElm.style.setProperty('--userscript-resizing-secondary-inner-height', secondaryInner?secondaryInner.offsetHeight:'')
  1580. if(!secondaryInner)full=false;
  1581. })();
  1582.  
  1583. }
  1584.  
  1585.  
  1586. window.addEventListener('resize',function(){
  1587. const cssElm=document.querySelector('ytd-watch-flexy');
  1588. if(cssElm){
  1589.  
  1590.  
  1591. }
  1592.  
  1593.  
  1594. }) */
  1595.  
  1596. document.addEventListener('wheel',function(evt){
  1597. if(!scriptEnable)return;
  1598. if(displayedPlaylist && displayedPlaylist.contains(evt.target)){
  1599. evt.stopPropagation(); evt.stopImmediatePropagation()
  1600. }
  1601. },{capture:true,passive:true});
  1602.  
  1603.  
  1604. function setVideosTwoColumns(flag, bool){
  1605.  
  1606. //two columns to one column
  1607.  
  1608. /*
  1609. [placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy
  1610.  
  1611. is-two-columns ="" => no is-two-columns
  1612. [placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer
  1613. no hidden => hidden =""
  1614. [placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer
  1615. hidden ="" => no hidden
  1616.  
  1617. */
  1618.  
  1619. let cssSelector1='[placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy'
  1620.  
  1621. let cssSelector2='[placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer'
  1622.  
  1623. let cssSelector3='[placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer'
  1624.  
  1625. let res={}
  1626. if(flag&1){
  1627.  
  1628. res.m1=$(cssSelector1)[0]
  1629. if(bool) $(res.m1).attr('is-two-columns',''); else $(res.m1).removeAttr('is-two-columns')
  1630. }
  1631.  
  1632. if(flag&2){
  1633. res.m2=$(cssSelector2)[0]
  1634. if(bool) $(res.m2).removeAttr('hidden'); else $(res.m2).attr('hidden','')
  1635. }
  1636.  
  1637. if(flag&4){
  1638. res.m3=$(cssSelector3)[0]
  1639. if(bool) $(res.m3).attr('hidden',''); else $(res.m3).removeAttr('hidden');
  1640. }
  1641.  
  1642.  
  1643. return res
  1644.  
  1645.  
  1646. }
  1647. /*
  1648. document.addEventListener('column1',function(evt){
  1649.  
  1650. console.log(evt)
  1651. document.aab=setVideosTwoColumns(1|2|4, false);
  1652. })
  1653. document.addEventListener('column2',function(evt){
  1654.  
  1655. console.log(evt)
  1656. document.aab=setVideosTwoColumns(1|2|4, true);
  1657. })
  1658. */
  1659.  
  1660. let lastScrollFetch=0;
  1661. function isScrolledToEnd(){
  1662. return (window.innerHeight + window.pageYOffset) >= document.scrollingElement.scrollHeight - 2;
  1663. }
  1664. let lastOffsetTop = 0;
  1665. window.addEventListener('scroll',function(evt){
  1666.  
  1667. //console.log(evt.target)
  1668.  
  1669. if(!scriptEnable)return;
  1670.  
  1671.  
  1672. if( !scrollingVideosList ) return;
  1673.  
  1674.  
  1675.  
  1676. let visibleHeight = document.scrollingElement.clientHeight;
  1677. let totalHeight = document.scrollingElement.scrollHeight;
  1678.  
  1679. if(totalHeight<visibleHeight*1.5)return; // filter out two column view;
  1680.  
  1681. let z=window.pageYOffset+visibleHeight;
  1682. let h=totalHeight - 40;
  1683. let h_advanced= h - ((visibleHeight>5*40)?visibleHeight*0.5:0);
  1684.  
  1685.  
  1686.  
  1687. if(z>h_advanced && !isWideScreenWithTwoColumns() ){
  1688.  
  1689. if(new Date - lastScrollFetch<500) return; //prevent continuous calling
  1690.  
  1691. lastScrollFetch=+new Date;
  1692. let res= setVideosTwoColumns(2|4, true)
  1693. if(res.m3 && res.m2){
  1694.  
  1695. //wait for DOM change, just in case
  1696. requestAnimationFrame(()=>{
  1697. let {offsetTop}=res.m2 // as visiblity of m2 & m3 switched.
  1698.  
  1699. if(offsetTop-lastOffsetTop<40) return; // in case bug, or repeating calling. // the next button shall below the this button
  1700. lastOffsetTop= offsetTop
  1701.  
  1702. res.m2.parentNode.dispatchEvent(new Event('yt-service-request-sent-button-renderer'))
  1703. res= null
  1704. })
  1705.  
  1706. }else{
  1707. res= null
  1708. }
  1709.  
  1710.  
  1711. }
  1712.  
  1713.  
  1714.  
  1715.  
  1716. },{passive:true})
  1717.  
  1718.  
  1719.  
  1720.  
  1721.  
  1722.  
  1723.  
  1724.  
  1725.  
  1726.  
  1727.  
  1728.  
  1729. // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js
  1730.  
  1731. }
  1732.  
  1733.  
  1734. ;!(function $$() {
  1735. 'use strict';
  1736.  
  1737. if(document.documentElement==null) return window.requestAnimationFrame($$)
  1738.  
  1739. var cssTxt = GM_getResourceText("contentCSS");
  1740.  
  1741. function addStyle (styleText) {
  1742. const styleNode = document.createElement('style');
  1743. styleNode.type = 'text/css';
  1744. styleNode.textContent = styleText;
  1745. document.documentElement.appendChild(styleNode);
  1746. return styleNode;
  1747. }
  1748.  
  1749.  
  1750. addStyle (cssTxt);
  1751.  
  1752. main(window.$);
  1753.  
  1754.  
  1755. // Your code here...
  1756. })();