Tabview Youtube

Make comments and lists into tabs for YouTube Videos

当前为 2022-05-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Tabview Youtube
  3. // @name:en Tabview Youtube
  4. // @name:jp Tabview Youtube
  5. // @name:zh-tw Tabview Youtube
  6. // @name:zh-cn Tabview Youtube
  7. // @namespace http://tampermonkey.net/
  8. // @version 2.3.1
  9. // @license MIT https://github.com/cyfung1031/Tabview-Youtube/blob/main/LICENSE
  10. // @description Make comments and lists into tabs for YouTube Videos
  11. // @description:en Make comments and lists into tabs for YouTube Videos
  12. // @description:jp YouTube動画のコメントやリストなどをタブに作成します
  13. // @description:zh-tw 把Youtube Videos中的評論及影片清單製作成Tabs
  14. // @description:zh-cn 把Youtube Videos中的评论及视频列表制作成Tabs
  15. // @author CY Fung
  16. // @include https://www.youtube.com/*
  17. // @icon https://github.com/cyfung1031/Tabview-Youtube/raw/main/images/icon128p.png
  18. // @run-at document-start
  19. // @grant GM_getResourceText
  20. // @resource contentCSS https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube/fe4e6d90673d5524207b77c1bde073aa80f2c041/css/style_content.css
  21. // @resource injectionJS1 https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube/fe4e6d90673d5524207b77c1bde073aa80f2c041/js/injection_script_1.js
  22. // @resource injectionFixAutoComplete https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube/fe4e6d90673d5524207b77c1bde073aa80f2c041/js/injectionScript_fixAutoComplete.js
  23. // @require https://cdnjs.cloudflare.com/ajax/libs/cash/8.1.0/cash.min.js
  24. // @noframes
  25. // ==/UserScript==
  26.  
  27. /* jshint esversion:6 */
  28.  
  29. function main($){
  30. // MIT License
  31. // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js
  32.  
  33.  
  34.  
  35.  
  36.  
  37.  
  38.  
  39.  
  40.  
  41.  
  42.  
  43.  
  44.  
  45.  
  46.  
  47.  
  48.  
  49.  
  50.  
  51.  
  52. -(function() {
  53.  
  54. function inIframe() {
  55. try {
  56. return window.self !== window.top;
  57. } catch (e) {
  58. return true;
  59. }
  60. }
  61.  
  62. if (inIframe()) return;
  63.  
  64. if (!$) return;
  65.  
  66. /**
  67. * SVG resources:
  68. * <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>
  69. */
  70.  
  71. const scriptVersionForExternal = '2022/05/07';
  72.  
  73. //const githubURLBase = "https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube";
  74. //const githubURLCommit = "bdf401045266e5224663f80b276bc7f56d122b8d";
  75.  
  76. const isMyScriptInChromeRuntime = () => typeof((((window || 0).chrome || 0).runtime || 0).getURL) == 'function'
  77.  
  78.  
  79. const svgComments = `
  80. <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
  81. 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
  82. 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
  83. 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
  84. 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"/>
  85. <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
  86. 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
  87. c3.27,0,5.93-2.66,5.93-5.929V8.465C60,5.195,57.34,2.535,54.072,2.535z"/>
  88. `.trim();
  89.  
  90. 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
  91. z M91,39h43v34H91V39z M61,259H30v-34h31V259z M61,73H30V39h31V73z M134,259H91v-34h43V259z M123,176.708v-55.417
  92. 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
  93. C128.84,188.011,123,184.958,123,176.708z M207,259h-43v-34h43V259z M207,73h-43V39h43V73z M268,259h-31v-34h31V259z M268,73h-31V39
  94. h31V73z"/>`.trim();
  95.  
  96. 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
  97. 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
  98. 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
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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
  106. 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
  107. c0.353,0.331,0.53,0.731,0.53,1.196C14.667,6.703,14.49,7.101,14.137,7.429z"/>`.trim();
  108.  
  109. const svgPlayList = `
  110. <rect x="0" y="64" width="256" height="42.667"/>
  111. <rect x="0" y="149.333" width="256" height="42.667"/>
  112. <rect x="0" y="234.667" width="170.667" height="42.667"/>
  113. <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
  114. 298.667,277.333 298.667,362.667 341.333,362.667 341.333,277.333 426.667,277.333 426.667,234.667"/>
  115. `.trim();
  116.  
  117. // --- Youtube Video Testing :
  118. // Square Video: https://www.youtube.com/watch?v=L0RXVnRbFg8
  119. // Square Video: https://www.youtube.com/watch?v=bK_rKhMIotU
  120. // ---
  121.  
  122.  
  123. const LAYOUT_TWO_COLUMNS = 1;
  124. const LAYOUT_THEATER = 2;
  125. const LAYOUT_FULLSCREEN = 4;
  126. const LAYOUT_CHATROOM = 8;
  127. const LAYOUT_CHATROOM_COLLASPED = 16;
  128. const LAYOUT_TAB_EXPANDED = 32;
  129. const LAYOUT_ENGAGEMENT_PANEL_EXPAND = 64;
  130.  
  131. const mtoInterval1 = 40;
  132. const mtoInterval2 = 150;
  133.  
  134. let lastVideoURL = null; // for less attribute set only
  135. /** @type {WeakRef | null} */
  136. const WeakRef = window.WeakRef;
  137. const mWeakRef = WeakRef ? (o => o ? new WeakRef(o) : null) : (o => o || null);
  138. /** @type {(wr: Object | null) => Object | null} */
  139. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  140.  
  141.  
  142.  
  143. const nonCryptoRandStr_base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  144. function nonCryptoRandStr(/** @type {number} */ n){
  145. const result = new Array(n);
  146. const baseStr = nonCryptoRandStr_base;
  147. const bLen = baseStr.length;
  148. for ( let i = 0; i < n; i++ ) {
  149. let t = null
  150. do {
  151. t = baseStr.charAt(Math.floor(Math.random() * bLen));
  152. }while(i===0 && 10-t>0)
  153. result[i]=t;
  154. }
  155. return result.join('');
  156. }
  157.  
  158. /**
  159. * Class definition
  160. * @property {string} propName - propriety description
  161. * ...
  162. */
  163. class ObserverRegister{
  164. constructor(/** @type {()=>MutationObserver | IntersectionObserver} */ observerCreator){
  165. let uid = null;
  166. const uidStore = ObserverRegister.uidStore;
  167. do{
  168. uid = nonCryptoRandStr(5);
  169. }while(uidStore[uid])
  170. uidStore[uid] = true;
  171. /**
  172. * uid is the unique string for each observer
  173. * @type {string}
  174. * @public
  175. */
  176. this.uid = uid;
  177. /**
  178. * observerCreator is a function to create the observer
  179. * @type {Function}
  180. * @public
  181. */
  182. this.observerCreator = observerCreator
  183. /**
  184. * observer is the actual observer object
  185. * @type {MutationObserver | IntersectionObserver}
  186. * @public
  187. */
  188. this.observer = null;
  189. }
  190. bindElement(/** @type {HTMLElement} */ elm, ...args){
  191. if(elm.hasAttribute(`o3r-${this.uid}`))return false;
  192. elm.setAttribute(`o3r-${this.uid}`,'')
  193. if(this.observer===null){
  194. this.observer=this.observerCreator();
  195. }
  196. this.observer.observe(elm, ...args)
  197. return true
  198. }
  199. clear(/** @type {boolean} */ flag){
  200. if(this.observer !== null){
  201. //const uidStore = ObserverRegister.uidStore;
  202. if(flag === true){
  203. this.observer.takeRecords();
  204. this.observer.disconnect();
  205. }
  206. this.observer = null;
  207. for(const s of document.querySelectorAll(`[o3r-${this.uid}]`)) s.removeAttribute(`o3r-${this.uid}`)
  208. //uidStore[this.uid]=false;
  209. //this.uid = null;
  210. }
  211. }
  212. }
  213.  
  214. /**
  215. * 'uidStore' is the static store of strings used.
  216. * @static
  217. */
  218. ObserverRegister.uidStore = {}; //backward compatible with FireFox 55.
  219.  
  220. const mtoMutation_watchFlexy = new ObserverRegister(()=>{
  221. return new MutationObserver(FP.mtoNavF)
  222. });
  223. const sa_wflexy = mtoMutation_watchFlexy.uid;
  224.  
  225. const mtoMutation_body = new ObserverRegister(()=>{
  226. return new MutationObserver(FP.mtoBodyF)
  227. });
  228. const sa_body = mtoMutation_body.uid;
  229.  
  230. const mtoFlexyAttr = new ObserverRegister(()=>{
  231. return new MutationObserver(mtf_attrFlexy)
  232. });
  233. const sa_flexyAttr = mtoFlexyAttr.uid;
  234.  
  235. const mtoVisibility_EngagementPanel = new ObserverRegister(()=>{
  236. return new MutationObserver(FP.mtf_attrEngagementPanel)
  237. });
  238. const sa_epanel = mtoVisibility_EngagementPanel.uid;
  239.  
  240. const mtoVisibility_Playlist = new ObserverRegister(()=>{
  241. return new AttributeMutationObserver({
  242. "hidden": FP.mtf_attrPlaylist
  243. })
  244. })
  245. const sa_playlist = mtoVisibility_Playlist.uid;
  246.  
  247. const mtoVisibility_Comments = new ObserverRegister(()=>{
  248. return new AttributeMutationObserver({
  249. "hidden": FP.mtf_attrComments
  250. })
  251. })
  252. const sa_comments = mtoVisibility_Comments.uid;
  253.  
  254.  
  255. const mtoVisibility_Chatroom = new ObserverRegister(()=>{
  256. return new AttributeMutationObserver({
  257. "collapsed": FP.mtf_attrChatroom
  258. })
  259. })
  260. const sa_chatroom = mtoVisibility_Chatroom.uid;
  261.  
  262.  
  263.  
  264.  
  265.  
  266. function tracer(key, cmp) {
  267. if (cmp > 0) return tracer[key] === cmp;
  268. return (tracer[key] = Date.now());
  269. }
  270.  
  271. function racer(key, f) {
  272. let now = Date.now();
  273. const kTime = `${key}$$1`
  274. let t = racer[kTime] || 0;
  275.  
  276. if (now < t) {
  277. const kCount = `${key}$$2`;
  278. racer[kCount] = (racer[kCount] || 0) + 1;
  279. if (racer[kCount] === 1) {
  280. let g = f;
  281. requestAnimationFrame(() => {
  282. racer[kCount] = 0;
  283. g();
  284. g = null;
  285. })
  286. }
  287. } else {
  288. racer[kTime] = now + 16;
  289. f();
  290. }
  291. }
  292.  
  293. class ScriptEF {
  294. constructor() {
  295. this._id = scriptEC;
  296. }
  297. isValid() {
  298. return this._id === scriptEC;
  299. }
  300. }
  301.  
  302. class Timeout {
  303.  
  304. set(f, d, repeatCount) {
  305. if (this.cid > 0) return;
  306. let sEF = new ScriptEF();
  307. if (repeatCount > 0) {
  308.  
  309. let rc = repeatCount;
  310. const g = () => {
  311. this.cid = 0;
  312. if (!sEF.isValid()) return;
  313. let res = f();
  314. if (--rc <= 0) return;
  315. if (res === true) this.cid = timeline.setTimeout(g, d);
  316. }
  317. g();
  318.  
  319. } else {
  320.  
  321. const g = () => {
  322. this.cid = 0;
  323. if (!sEF.isValid()) return;
  324. if (f() === true) this.cid = timeline.setTimeout(g, d);
  325. }
  326. this.cid = timeline.setTimeout(g, d);
  327. }
  328. }
  329.  
  330. clear() {
  331. if (this.cid > 0) timeline.clearTimeout(this.cid);
  332. }
  333.  
  334. isEmpty() {
  335. return !this.cid
  336. }
  337.  
  338.  
  339. }
  340.  
  341. class Mutex {
  342.  
  343. constructor() {
  344. this.p = Promise.resolve()
  345. }
  346.  
  347. lockWith(f) {
  348.  
  349. this.p = this.p.then(() => {
  350. return new Promise(f)
  351. }).catch(console.warn)
  352. }
  353.  
  354. }
  355.  
  356.  
  357.  
  358. function prettyElm(/** @type {Element} */ elm) {
  359. if (!elm || !elm.nodeName) return null;
  360. const eId = elm.id || null;
  361. const eClsName = elm.className || null;
  362. return [elm.nodeName.toLowerCase(), typeof eId == 'string' ? "#" + eId : '', typeof eClsName == 'string' ? '.' + eClsName.replace(/\s+/g, '.') : ''].join('').trim();
  363. }
  364.  
  365. function extractTextContent(/** @type {Node} */ elm) {
  366. 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, '')
  367. }
  368.  
  369. function addScript(/** @type {string} */ scriptText) {
  370. const scriptNode = document.createElement('script');
  371. scriptNode.type = 'text/javascript';
  372. scriptNode.textContent = scriptText;
  373. try {
  374. document.documentElement.appendChild(scriptNode);
  375. } catch (e) {
  376. console.log('addScript Error', e)
  377. }
  378. return scriptNode;
  379. }
  380.  
  381. function addScriptByURL(/** @type {string} */ scriptURL) {
  382. const scriptNode = document.createElement('script');
  383. scriptNode.type = 'text/javascript';
  384. scriptNode.src = scriptURL;
  385. try {
  386. document.documentElement.appendChild(scriptNode);
  387. } catch (e) {
  388. console.log('addScriptByURL Error', e)
  389. }
  390. return scriptNode;
  391. }
  392.  
  393. function addStyle(/** @type {string} */ styleText, /** @type {HTMLElement | Document} */ container) {
  394. const styleNode = document.createElement('style');
  395. //styleNode.type = 'text/css';
  396. styleNode.textContent = styleText;
  397. (container || document.documentElement).appendChild(styleNode);
  398. return styleNode;
  399. }
  400.  
  401.  
  402. const stopIframePropagation = function(/** @type {Event} */ evt){
  403. if(scriptEnable && evt && evt.target && evt.target.nodeName=='IFRAME'){
  404. evt.stopImmediatePropagation();
  405. evt.stopPropagation();
  406. }
  407. }
  408. document.addEventListener('mouseover', stopIframePropagation, true)
  409. document.addEventListener('mouseout', stopIframePropagation, true)
  410. document.addEventListener('mousedown', stopIframePropagation, true)
  411. document.addEventListener('mouseup', stopIframePropagation, true)
  412. document.addEventListener('keydown', stopIframePropagation, true)
  413. document.addEventListener('keyup', stopIframePropagation, true)
  414. document.addEventListener('mouseenter', stopIframePropagation, true)
  415. document.addEventListener('mouseleave', stopIframePropagation, true)
  416.  
  417.  
  418.  
  419. function isDOMVisible(/** @type {HTMLElement} */ elem) {
  420. // jQuery version : https://github.com/jquery/jquery/blob/a684e6ba836f7c553968d7d026ed7941e1a612d8/src/css/hiddenVisibleSelectors.js
  421. return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
  422. }
  423.  
  424. function isNonEmptyString(s) {
  425. return typeof s == 'string' && s.length > 0;
  426. }
  427.  
  428.  
  429. async function nativeFunc(/** @type {EventTarget} */ dom, /** @type {string} */ property, /** @type {any} */ args) {
  430. dom.dispatchEvent(new CustomEvent("userscript-call-dom-func", { detail: { property, args } }))
  431. }
  432.  
  433. // async function nativeValue(dom, property, args) {
  434. // dom.dispatchEvent(new CustomEvent("userscript-call-dom-value", { detail: { property, args } }))
  435. // }
  436. // async function nativeFuncStacked(/** @type {string} */ selector, /** @type {string} */ property, /** @type {any} */ args){
  437. // document.dispatchEvent(new CustomEvent("userscript-call-dom-func-stacked", { detail: { selector, property, args } }))
  438. // }
  439. // async function nativeValueStacked(selector, property, args){
  440. // document.dispatchEvent(new CustomEvent("userscript-call-dom-value-stacked", { detail: { selector, property, args } }))
  441. // }
  442. // async function nativeConstStacked(selector, property, args){
  443. // document.dispatchEvent(new CustomEvent("userscript-call-dom-const-stacked", { detail: { selector, property, args } }))
  444. // }
  445.  
  446.  
  447. function akAttr(/** @type {HTMLElement} */ cssElm, /** @type {String} */ attrName, /** @type {boolean} */ isNegative, /** @type {string | any} */ flag) {
  448. // isNegative => incomplete loading
  449.  
  450. let u = parseInt(cssElm.getAttribute(attrName) || 0) || 0;
  451. let ak = Math.abs(u);
  452.  
  453. if (ak > 100 && isNegative && u < 0) {
  454.  
  455. } else if (ak > 100 && !isNegative && u > 0) {
  456.  
  457. } else {
  458. if (ak <= 100) {
  459. ak = 101;
  460. } else {
  461. ak++;
  462. if (ak >= 800) ak = 101;
  463. }
  464. // 101, 102, ... 799, 101
  465. }
  466.  
  467. cssElm.setAttribute(attrName, `${ isNegative ? -ak : ak }${ flag || '' }`)
  468. }
  469.  
  470.  
  471.  
  472. let timeout_resize_for_layout_change = new Timeout();
  473.  
  474. function dispatchWindowResize(){
  475. // for youtube to detect layout resize for adjusting Player tools
  476. return window.dispatchEvent(new Event('resize'));
  477. }
  478.  
  479.  
  480.  
  481. function layoutStatusChanged(/** @type {number} */ old_layoutStatus, /** @type {number} */ new_layoutStatus) {
  482.  
  483.  
  484. if (old_layoutStatus === new_layoutStatus) return;
  485.  
  486. const cssElm = kRef(ytdFlexy);
  487.  
  488. if (!cssElm) return;
  489.  
  490.  
  491. const new_isExpandedChat = !(new_layoutStatus & LAYOUT_CHATROOM_COLLASPED) && (new_layoutStatus & LAYOUT_CHATROOM)
  492. const new_isCollaspedChat = (new_layoutStatus & LAYOUT_CHATROOM_COLLASPED) && (new_layoutStatus & LAYOUT_CHATROOM)
  493.  
  494. const new_isTwoColumns = !!(new_layoutStatus & LAYOUT_TWO_COLUMNS);
  495. const new_isTheater = !!(new_layoutStatus & LAYOUT_THEATER);
  496. const new_isTabExpanded = !!(new_layoutStatus & LAYOUT_TAB_EXPANDED);
  497. const new_isFullScreen = !!(new_layoutStatus & LAYOUT_FULLSCREEN);
  498. const new_isExpandEPanel = !!(new_layoutStatus & LAYOUT_ENGAGEMENT_PANEL_EXPAND);
  499.  
  500.  
  501.  
  502. function showTabOrChat() {
  503.  
  504. layoutStatusMutex.lockWith(unlock => {
  505.  
  506. if (lastShowTab == '#chatroom') {
  507.  
  508. if (new_isTabExpanded) switchTabActivity(null)
  509. if (!new_isExpandedChat) ytBtnExpandChat();
  510.  
  511. } else if (lastShowTab && lastShowTab.indexOf('#engagement-panel-') == 0) {
  512.  
  513. if (new_isTabExpanded) switchTabActivity(null)
  514. if (!new_isExpandEPanel) ytBtnOpenEngagementPanel(lastShowTab);
  515.  
  516. } else {
  517.  
  518. if (new_isExpandedChat) ytBtnCollapseChat()
  519. if (!new_isTabExpanded) {setToActiveTab();}
  520.  
  521. }
  522.  
  523. timeline.setTimeout(unlock, 40);
  524.  
  525. })
  526. }
  527.  
  528. function hideTabAndChat() {
  529.  
  530. layoutStatusMutex.lockWith(unlock => {
  531.  
  532. if (new_isTabExpanded) switchTabActivity(null)
  533. if (new_isExpandedChat) ytBtnCollapseChat()
  534. if (new_isExpandEPanel) ytBtnCloseEngagementPanels();
  535.  
  536.  
  537. timeline.setTimeout(unlock, 40);
  538.  
  539. })
  540.  
  541. }
  542.  
  543.  
  544. if (new_isExpandedChat || new_isTabExpanded || new_isExpandEPanel) {
  545. if (statusCollasped !== 1) statusCollasped = 1;
  546. } else {
  547. if (statusCollasped === 1) statusCollasped = 2;
  548. }
  549.  
  550. let changes = 0;
  551.  
  552. if (old_layoutStatus !== null) changes = old_layoutStatus ^ new_layoutStatus;
  553.  
  554. let chat_collasped_changed = !!(changes & LAYOUT_CHATROOM_COLLASPED)
  555. let tab_expanded_changed = !!(changes & LAYOUT_TAB_EXPANDED)
  556. let theater_mode_changed = !!(changes & LAYOUT_THEATER)
  557. let column_mode_changed = !!(changes & LAYOUT_TWO_COLUMNS)
  558. let fullscreen_mode_changed = !!(changes & LAYOUT_FULLSCREEN)
  559. let epanel_expanded_changed = !!(changes & LAYOUT_ENGAGEMENT_PANEL_EXPAND)
  560.  
  561. let tab_change = (tab_expanded_changed ? 1 : 0) | (chat_collasped_changed ? 2 : 0) | (epanel_expanded_changed ? 4 : 0);
  562.  
  563. let isChatOrTabExpandTriggering = tab_change == 0 ? false : (
  564. (tab_expanded_changed && new_isTabExpanded) ||
  565. (chat_collasped_changed && new_isExpandedChat) ||
  566. (epanel_expanded_changed && new_isExpandEPanel)
  567. );
  568.  
  569. let isChatOrTabCollaspeTriggering = tab_change == 0 ? false : (
  570. (tab_expanded_changed && !new_isTabExpanded) ||
  571. (chat_collasped_changed && new_isCollaspedChat) ||
  572. (epanel_expanded_changed && !new_isExpandEPanel)
  573. );
  574.  
  575.  
  576.  
  577. let moreThanOneShown = (new_isTabExpanded + new_isExpandedChat + new_isExpandEPanel) > 1
  578.  
  579. let requestVideoResize = false;
  580.  
  581. if (fullscreen_mode_changed || new_isFullScreen) {
  582.  
  583. } else if(new_isTwoColumns && !new_isTheater && !column_mode_changed && (tab_change==2 || tab_change==4) && !new_isTabExpanded && new_isExpandedChat && new_isExpandEPanel){
  584.  
  585. if(epanel_expanded_changed){
  586. layoutStatusMutex.lockWith(unlock => {
  587. ytBtnCollapseChat();
  588. setTimeout(unlock,13)
  589. })
  590. }else if(chat_collasped_changed){
  591. layoutStatusMutex.lockWith(unlock => {
  592. ytBtnCloseEngagementPanels();
  593. setTimeout(unlock,13)
  594. })
  595.  
  596. }
  597.  
  598. } else if (tab_change == 0 && column_mode_changed && new_isTwoColumns && !new_isTheater && statusCollasped === 1 && moreThanOneShown) {
  599.  
  600. showTabOrChat();
  601. requestVideoResize = true;
  602.  
  603. } else if (tab_change == 2 && new_isExpandedChat && new_isTwoColumns && !new_isTheater && statusCollasped === 1 && new_isTabExpanded && !column_mode_changed) {
  604.  
  605. switchTabActivity(null);
  606. requestVideoResize = true;
  607.  
  608. } else if (isChatOrTabExpandTriggering && new_isTwoColumns && new_isTheater && statusCollasped === 1 && !theater_mode_changed && !column_mode_changed) {
  609.  
  610. ytBtnCancelTheater();
  611. requestVideoResize = true;
  612.  
  613. } else if (new_isTwoColumns && new_isTheater && statusCollasped === 1) {
  614.  
  615. hideTabAndChat();
  616. requestVideoResize = true;
  617.  
  618. } else if (isChatOrTabCollaspeTriggering && new_isTwoColumns && !new_isTheater && statusCollasped === 2 && !column_mode_changed) {
  619.  
  620. ytBtnSetTheater();
  621. requestVideoResize = true;
  622.  
  623. } else if (tab_change == 0 && (column_mode_changed || theater_mode_changed) && new_isTwoColumns && !new_isTheater && statusCollasped !== 1) {
  624.  
  625. showTabOrChat();
  626. requestVideoResize = true;
  627.  
  628. } else if (!new_isFullScreen && new_isTwoColumns && !new_isTheater && !new_isTabExpanded &&
  629. (new_isCollaspedChat || !new_isExpandedChat) &&
  630. !new_isExpandEPanel
  631. ) {
  632. // bug fix for restoring from mini player
  633.  
  634. layoutStatusMutex.lockWith(unlock => {
  635. setToActiveTab();
  636. timeline.setTimeout(unlock, 40);
  637. })
  638.  
  639. requestVideoResize = true;
  640.  
  641. } else if (tab_expanded_changed) {
  642.  
  643. requestVideoResize = true;
  644.  
  645. }
  646.  
  647.  
  648. if (column_mode_changed && !chat_collasped_changed && new_isExpandedChat) {
  649.  
  650. runAfterExpandChat();
  651.  
  652. }
  653.  
  654.  
  655.  
  656. if (requestVideoResize) {
  657.  
  658. timeout_resize_for_layout_change.clear();
  659. timeout_resize_for_layout_change.set(() => {
  660. dispatchWindowResize();
  661. }, 92)
  662.  
  663. } else if (timeout_resize_for_layout_change.isEmpty() && (Date.now()) - lastResizeAt > 600) {
  664. timeout_resize_for_layout_change.set(() => {
  665. if ((Date.now()) - lastResizeAt > 600) dispatchWindowResize();
  666. }, 62)
  667. }
  668.  
  669.  
  670.  
  671.  
  672. }
  673.  
  674.  
  675.  
  676. const wls = new Proxy({
  677. /** @type {number | null} */
  678. layoutStatus:undefined
  679. }, {
  680. get: function(target, prop) {
  681. return target[prop];
  682. },
  683. set: function(target, prop, value) {
  684. if(prop=='layoutStatus'){
  685. if (value === null) {
  686. statusCollasped = 0;
  687. target[prop] = value;
  688. return;
  689. }else if(target[prop]===value){
  690. return;
  691. }else{
  692. if (!target.layoutStatus_pending) {
  693. target.layoutStatus_pending = true;
  694. const old_layoutStatus = target[prop];
  695. target[prop] = value;
  696. layoutStatusMutex.lockWith(unlock => {
  697. target.layoutStatus_pending = false;
  698. layoutStatusChanged(old_layoutStatus, target[prop]);
  699. timeline.setTimeout(unlock, 40)
  700. })
  701. return;
  702. }
  703. }
  704. }
  705. target[prop] = value;
  706. },
  707. has: function(target, prop) {
  708. return (prop in target);
  709. }
  710. });
  711.  
  712.  
  713.  
  714.  
  715.  
  716.  
  717. const svgElm = (w, h, vw, vh, p) => `<svg width="${w}" height="${h}" viewBox="0 0 ${vw} ${vh}" preserveAspectRatio="xMidYMid meet">${p}</svg>`
  718.  
  719. let settings = {
  720. defaultTab: "#tab-videos"
  721. };
  722.  
  723.  
  724. let mtoInterval = mtoInterval1;
  725.  
  726. function isVideoPlaying(video) {
  727. return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > video.HAVE_CURRENT_DATA;
  728. }
  729.  
  730. function wAttr(elm, attr, kv) {
  731. if (!elm || kv === null) {} else if (kv === true) { elm.setAttribute(attr, '') } else if (kv === false) { elm.removeAttribute(attr) } else if (typeof kv == 'string') { elm.setAttribute(attr, kv) }
  732. }
  733.  
  734. function hideTabBtn(tabBtn) {
  735. let isActiveBefore = tabBtn.classList.contains('active');
  736. tabBtn.classList.add("tab-btn-hidden");
  737. if (isActiveBefore) {
  738. setToActiveTab();
  739. }
  740. }
  741.  
  742. function hasAttribute(obj, key) {
  743. return obj && obj.hasAttribute(key);
  744. }
  745.  
  746. function isTheater() {
  747. const cssElm = kRef(ytdFlexy);
  748. return (cssElm && cssElm.hasAttribute('theater'))
  749. }
  750.  
  751. function isFullScreen() {
  752. const cssElm = kRef(ytdFlexy);
  753. return (cssElm && cssElm.hasAttribute('fullscreen'))
  754. }
  755.  
  756. function isChatExpand() {
  757. const cssElm = kRef(ytdFlexy);
  758. return cssElm && cssElm.hasAttribute('userscript-chatblock') && !cssElm.hasAttribute('userscript-chat-collapsed')
  759. }
  760.  
  761. function isWideScreenWithTwoColumns() {
  762. const cssElm = kRef(ytdFlexy);
  763. return (cssElm && cssElm.hasAttribute('is-two-columns_'))
  764. }
  765.  
  766. function isAnyActiveTab() {
  767. return $('#right-tabs .tab-btn.active').length > 0
  768. }
  769.  
  770. function isEngagementPanelExpanded() { //note: not checking the visual elements
  771. const cssElm = kRef(ytdFlexy);
  772. return (cssElm && +cssElm.getAttribute('userscript-engagement-panel') > 0)
  773. }
  774.  
  775. function engagement_panels_() {
  776.  
  777. let res = [];
  778. let shownRes = [];
  779.  
  780. let v = 0,
  781. k = 1,
  782. count = 0;
  783. for (const ePanel of document.querySelectorAll(
  784. `ytd-watch-flexy ytd-engagement-panel-section-list-renderer[o3r-${sa_epanel}]`
  785. )) {
  786.  
  787. let visibility = ePanel.getAttribute('visibility') //ENGAGEMENT_PANEL_VISIBILITY_EXPANDED //ENGAGEMENT_PANEL_VISIBILITY_HIDDEN
  788.  
  789. switch (visibility) {
  790. case 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED':
  791. v |= k;
  792. count++;
  793. shownRes.push(ePanel)
  794. res.push({ ePanel, k, visible: true });
  795. break;
  796. case 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN':
  797. res.push({ ePanel, k, visible: false });
  798. break;
  799. default:
  800. res.push({ ePanel, k, visible: false });
  801. }
  802.  
  803. k = k << 1;
  804.  
  805. }
  806. return { list: res, value: v, count: count, shownRes };
  807. }
  808.  
  809.  
  810. function ytBtnOpenEngagementPanel(/** @type {number | string} */ panel_id) {
  811.  
  812. if (typeof panel_id == 'string') {
  813. panel_id = panel_id.replace('#engagement-panel-', '');
  814. panel_id = parseInt(panel_id);
  815. }
  816. if (panel_id >= 0) {} else return false;
  817.  
  818. let panels = engagement_panels_();
  819.  
  820. for (const { ePanel, k, visible } of panels.list) {
  821. if ((panel_id & k) === k) {
  822. if (!visible) ePanel.setAttribute('visibility', "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED");
  823. } else {
  824. if (visible) ytBtnCloseEngagementPanel(ePanel);
  825. }
  826. }
  827.  
  828. }
  829.  
  830. function ytBtnCloseEngagementPanel(/** @type {HTMLElement} */ s) {
  831. //ePanel.setAttribute('visibility',"ENGAGEMENT_PANEL_VISIBILITY_HIDDEN");
  832. let btn = s.querySelector('ytd-watch-flexy ytd-engagement-panel-title-header-renderer #header > #visibility-button > ytd-button-renderer');
  833. if (btn) {
  834. btn.click();
  835. }
  836. }
  837.  
  838. function ytBtnCloseEngagementPanels() {
  839. if (isEngagementPanelExpanded()) {
  840. for (const s of document.querySelectorAll(
  841. `ytd-watch-flexy ytd-engagement-panel-section-list-renderer[o3r-${sa_epanel}]`
  842. )) {
  843. if (s.getAttribute('visibility') == "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED") ytBtnCloseEngagementPanel(s);
  844. }
  845. }
  846. }
  847.  
  848. function ytBtnSetTheater() {
  849. if (!isTheater()) {
  850. const sizeBtn = document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
  851. if (sizeBtn) sizeBtn.click();
  852. }
  853. }
  854.  
  855. function ytBtnCancelTheater() {
  856. if (isTheater()) {
  857. const sizeBtn = document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
  858. if (sizeBtn) sizeBtn.click();
  859. }
  860. }
  861.  
  862. function ytBtnExpandChat() {
  863. let button = document.querySelector('ytd-live-chat-frame#chat[collapsed] > .ytd-live-chat-frame#show-hide-button')
  864. if (button) button.querySelector('ytd-toggle-button-renderer').click();
  865. }
  866.  
  867. function ytBtnCollapseChat() {
  868. let button = document.querySelector('ytd-live-chat-frame#chat:not([collapsed]) > .ytd-live-chat-frame#show-hide-button')
  869. if (button) button.querySelector('ytd-toggle-button-renderer').click();
  870. }
  871.  
  872. /*
  873. function hackImgShadow(imgShadow) {
  874. // add to #columns and add back after loaded
  875. let img = imgShadow.querySelector('img')
  876. if (!img) return;
  877.  
  878. let p = imgShadow.parentNode
  879. let z = $(imgShadow).clone()[0]; //to occupy the space
  880. p.replaceChild(z, imgShadow)
  881. $(imgShadow).prependTo('#columns'); // refer to css hack
  882.  
  883. function onload(evt) {
  884. if (evt) this.removeEventListener('load', onload, false)
  885. p.replaceChild(imgShadow, z)
  886. p = null;
  887. z = null;
  888. imgShadow = null;
  889. }
  890.  
  891. if (img.complete) onload();
  892. else img.addEventListener('load', onload, false)
  893. }
  894. */
  895.  
  896.  
  897. const Q = {}
  898.  
  899.  
  900.  
  901. function chatFrameContentDocument() {
  902. // non-null if iframe exist && contentDocument && readyState = complete
  903. /** @type {HTMLIFrameElement | null} */
  904. let iframe = document.querySelector('ytd-live-chat-frame iframe#chatframe');
  905. if (!iframe) return null; //iframe must be there
  906. /** @type {Document | null} */
  907. let cDoc = null;
  908. try {
  909. cDoc = iframe.contentDocument;
  910. } catch (e) {}
  911. if (!cDoc) return null;
  912. if (cDoc.readyState != 'complete') return null; //we must wait for its completion
  913.  
  914. return cDoc;
  915.  
  916. }
  917.  
  918. function chatFrameElement(/** @type {string} */ cssSelector) {
  919. let cDoc = chatFrameContentDocument();
  920. if (!cDoc) return null;
  921. /** @type {HTMLElement | null} */
  922. let elm = null;
  923. try {
  924. elm = cDoc.querySelector(cssSelector)
  925. } catch (e) {
  926. console.log('iframe error', e)
  927. }
  928. return elm;
  929. }
  930.  
  931.  
  932.  
  933.  
  934. function fixTabs() {
  935.  
  936.  
  937. if (!scriptEnable) return;
  938.  
  939.  
  940. let queryElement = document.querySelector('*:not(#tab-videos) > #related:not([non-placeholder-videos]) > ytd-watch-next-secondary-results-renderer')
  941.  
  942. let isRelocated = !!queryElement;
  943.  
  944.  
  945.  
  946. if (isRelocated) {
  947.  
  948. let relocatedRelated = queryElement.parentNode; // NOT NULL
  949.  
  950. let right_tabs = document.querySelector('#right-tabs');
  951. let tab_videos = right_tabs.querySelector("#tab-videos");
  952.  
  953. if (!right_tabs || !tab_videos) return;
  954.  
  955. for (const s of relocatedRelated.querySelectorAll('#related')) {
  956. s.setAttribute('non-placeholder-videos', '')
  957. }
  958.  
  959. let target_container = document.querySelector('ytd-watch-flexy:not([is-two-columns_]) #primary-inner, ytd-watch-flexy[is-two-columns_] #secondary-inner')
  960.  
  961. if (target_container) target_container.append(right_tabs) // last-child
  962.  
  963.  
  964. let videos_related = relocatedRelated; // NOT NULL
  965. $('[placeholder-videos]').removeAttr('placeholder-videos');
  966. $('[placeholder-for-youtube-play-next-queue]').removeAttr('placeholder-for-youtube-play-next-queue');
  967.  
  968. tab_videos.appendChild(videos_related);
  969. let videos_results_renderer = relocatedRelated.querySelector("ytd-watch-next-secondary-results-renderer");
  970. if (videos_results_renderer) videos_results_renderer.setAttribute('data-dom-changed-by-tabview-youtube', scriptVersionForExternal);
  971. videos_related.setAttribute('placeholder-for-youtube-play-next-queue', '')
  972. videos_related.setAttribute('placeholder-videos', '')
  973.  
  974. $('[placeholder-videos]').on("scroll", makeBodyScrollByEvt);
  975.  
  976.  
  977.  
  978.  
  979. }
  980.  
  981.  
  982.  
  983. /** @type {HTMLElement | null} */
  984. let chatroom = null;
  985. if (chatroom = document.querySelector('*:not([data-positioner="before|#chat"]) + ytd-live-chat-frame#chat, ytd-live-chat-frame#chat:first-child')) {
  986.  
  987. let positioner = document.querySelector('tabview-youtube-positioner[data-positioner="before|#chat"]');
  988. if (positioner) positioner.remove();
  989.  
  990.  
  991. if (document.querySelector('.YouTubeLiveFilledUpView')) {
  992. // no relocation
  993. } else {
  994.  
  995. $(chatroom).insertBefore('#right-tabs')
  996.  
  997. }
  998.  
  999.  
  1000. $(positioner ? positioner : document.createElement('tabview-youtube-positioner')).attr('data-positioner', 'before|#chat').insertBefore(chatroom)
  1001.  
  1002.  
  1003.  
  1004. }
  1005.  
  1006.  
  1007. }
  1008.  
  1009. function handlerAutoCompleteExist() {
  1010.  
  1011.  
  1012. /** @type {HTMLElement} */
  1013. let autoComplete = this;
  1014.  
  1015. autoComplete.removeEventListener('autocomplete-sc-exist', handlerAutoCompleteExist, false)
  1016.  
  1017. let domId = autoComplete.getAttribute('data-autocomplete-input-id')
  1018. let searchBox = autoComplete.ownerDocument.querySelector(`[data-autocomplete-results-id="${domId}"]`)
  1019.  
  1020. if (!domId || !searchBox) return;
  1021.  
  1022. let positioner = searchBox.nextSibling;
  1023. if (positioner && positioner.nodeName.toLowerCase() == "autocomplete-positioner") {} else if (positioner && positioner.nodeName.toLowerCase() != "autocomplete-positioner") {
  1024. $(positioner = document.createElement("autocomplete-positioner")).insertAfter(searchBox);
  1025. } else {
  1026. $(positioner = document.createElement("autocomplete-positioner")).prependTo(searchBox.parentNode);
  1027. }
  1028. $(autoComplete).prependTo(positioner);
  1029.  
  1030. positioner.style.setProperty('--sb-margin-bottom', getComputedStyle(searchBox).marginBottom)
  1031. positioner.style.setProperty('--height', searchBox.offsetHeight + 'px')
  1032.  
  1033. }
  1034.  
  1035. function mtf_fixAutoCompletePosition(/** @type {HTMLElement} */ elmAutoComplete) {
  1036.  
  1037.  
  1038. elmAutoComplete.setAttribute('autocomplete-disable-updatesc', '')
  1039. elmAutoComplete.addEventListener('autocomplete-sc-exist', handlerAutoCompleteExist, false)
  1040.  
  1041. if(document.querySelector('script#userscript-tabview-injection-facp')) return;
  1042.  
  1043. if (isMyScriptInChromeRuntime()){
  1044. addScriptByURL(window.chrome.runtime.getURL('js/injectionScript_fixAutoComplete.js')).id = 'userscript-tabview-injection-facp';;
  1045. } else {
  1046. let injection_script = GM_getResourceText("injectionFixAutoComplete")
  1047. addScript(`${injection_script}`).id = 'userscript-tabview-injection-facp';;
  1048. }
  1049.  
  1050. }
  1051.  
  1052. function mtf_AfterFixTabs() {
  1053.  
  1054.  
  1055. /** @type {HTMLElement | null} */
  1056. let ytdFlexyElm = kRef(ytdFlexy);
  1057. if (!scriptEnable || !ytdFlexyElm) return;
  1058.  
  1059. /** @type {HTMLElement | null} */
  1060. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1061.  
  1062.  
  1063.  
  1064. const autocomplete = rootElement.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search + autocomplete-positioner > .autocomplete-suggestions[data-autocomplete-input-id]:not([position-fixed-by-tabview-youtube])')
  1065.  
  1066. if (autocomplete) {
  1067.  
  1068. const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  1069.  
  1070.  
  1071. if (searchBox) {
  1072.  
  1073.  
  1074. autocomplete.parentNode.setAttribute('position-fixed-by-tabview-youtube', '');
  1075. autocomplete.setAttribute('position-fixed-by-tabview-youtube', '');
  1076. autocomplete.setAttribute('userscript-scrollbar-render', '')
  1077.  
  1078. if (!searchBox.hasAttribute('is-set-click-to-toggle')) {
  1079. searchBox.setAttribute('is-set-click-to-toggle', '')
  1080. searchBox.addEventListener('click', function() {
  1081.  
  1082.  
  1083. setTimeout(() => {
  1084. const autocomplete = document.querySelector(`.autocomplete-suggestions[data-autocomplete-input-id="${ this.getAttribute('data-autocomplete-results-id') }"]`)
  1085.  
  1086. if (!autocomplete) return;
  1087.  
  1088. const isNotEmpty = (autocomplete.textContent || '').length > 0 && (this.value || '').length > 0;
  1089.  
  1090. if (isNotEmpty) {
  1091.  
  1092. let elmVisible = isDOMVisible(autocomplete)
  1093.  
  1094. if (elmVisible) $(autocomplete).hide();
  1095. else $(autocomplete).show();
  1096.  
  1097. }
  1098.  
  1099. }, 20);
  1100.  
  1101. })
  1102.  
  1103. let timeoutOnce_searchbox_keyup = new Timeout();
  1104. searchBox.addEventListener('keyup', function() {
  1105.  
  1106. timeoutOnce_searchbox_keyup.set(() => {
  1107.  
  1108. const autocomplete = document.querySelector(`.autocomplete-suggestions[data-autocomplete-input-id="${ this.getAttribute('data-autocomplete-results-id') }"]`)
  1109.  
  1110. if (!autocomplete) return;
  1111.  
  1112.  
  1113. const isNotEmpty = (autocomplete.textContent || '').length > 0 && (this.value || '').length > 0
  1114.  
  1115. if (isNotEmpty) {
  1116.  
  1117. let elmVisible = isDOMVisible(autocomplete)
  1118.  
  1119. if (!elmVisible) $(autocomplete).show();
  1120.  
  1121. }
  1122.  
  1123. }, 20);
  1124.  
  1125. })
  1126.  
  1127. }
  1128.  
  1129.  
  1130.  
  1131. }
  1132.  
  1133. }
  1134.  
  1135.  
  1136.  
  1137.  
  1138. let currentLastVideo = rootElement.querySelector('[placeholder-videos] #items ytd-compact-video-renderer:last-of-type')
  1139. let prevLastVideo = kRef(_cachedLastVideo);
  1140.  
  1141. if (prevLastVideo !== currentLastVideo && currentLastVideo) {
  1142. _cachedLastVideo = mWeakRef(currentLastVideo);
  1143. }
  1144.  
  1145. if (prevLastVideo !== currentLastVideo && currentLastVideo && prevLastVideo) {
  1146.  
  1147. let isPrevRemoved = !prevLastVideo.parentNode
  1148.  
  1149.  
  1150. function getVideoListHash() {
  1151.  
  1152. let res = [...document.querySelectorAll('[placeholder-videos] #items ytd-compact-video-renderer')].map(renderer => {
  1153. return renderer.querySelector('a[href*="watch"][href*="v="]').getAttribute('href')
  1154.  
  1155. }).join('|')
  1156. // /watch?v=XXXXX|/watch?v=XXXXXX|/watch?v=XXXXXX
  1157.  
  1158. // alternative - DOM.data.videoId
  1159. // let elms = document.querySelectorAll('[placeholder-videos] #items ytd-compact-video-renderer')
  1160. // let res = [...elms].map(elm=>elm.data.videoId||'').join('|') ;
  1161.  
  1162. if (res.indexOf('||') >= 0) {
  1163. res = '';
  1164. }
  1165.  
  1166. return res ? res : null;
  1167. }
  1168.  
  1169. if (isPrevRemoved) {
  1170.  
  1171. // this is the replacement of videos instead of addition
  1172.  
  1173. const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  1174.  
  1175. let currentPlayListHash = getVideoListHash() || null;
  1176.  
  1177. if (!currentPlayListHash) {
  1178.  
  1179. } else if (!videoListBeforeSearch && searchBox) {
  1180.  
  1181. videoListBeforeSearch = currentPlayListHash;
  1182. if (videoListBeforeSearch) {
  1183. //console.log('fromSearch', videoListBeforeSearch)
  1184.  
  1185. requestAnimationFrame(function() {
  1186.  
  1187. let renderer = document.querySelector('[placeholder-videos] ytd-watch-next-secondary-results-renderer');
  1188. if (searchBox && searchBox.parentNode) searchBox.blur();
  1189.  
  1190. if (renderer) {
  1191. let scrollParent = renderer.parentNode;
  1192. if (scrollParent.scrollHeight > scrollParent.offsetHeight) {
  1193. let targetTop = renderer.offsetTop;
  1194. if (searchBox && searchBox.parentNode == scrollParent) targetTop -= searchBox.offsetHeight
  1195. scrollParent.scrollTop = targetTop - scrollParent.firstChild.offsetTop;
  1196. }
  1197. }
  1198.  
  1199. });
  1200.  
  1201. }
  1202.  
  1203. } else if (videoListBeforeSearch) {
  1204.  
  1205. if (currentPlayListHash != videoListBeforeSearch) {
  1206.  
  1207. videoListBeforeSearch = null;
  1208. //console.log('fromSearch', videoListBeforeSearch)
  1209.  
  1210.  
  1211. }
  1212.  
  1213. }
  1214.  
  1215.  
  1216. }
  1217.  
  1218.  
  1219. }
  1220.  
  1221.  
  1222.  
  1223.  
  1224. }
  1225.  
  1226. function base_ChatExist() {
  1227.  
  1228. let ytdFlexyElm = kRef(ytdFlexy);
  1229. if (!scriptEnable || !ytdFlexyElm) return null;
  1230.  
  1231. // no mutation triggering if the changes are inside the iframe
  1232.  
  1233. // 1) Detection of #continuations inside iframe
  1234. // iframe ownerDocument is accessible due to same origin
  1235. // if the chatroom is collasped, no determination of live chat or replay (as no #continuations and somehow a blank iframe doc)
  1236.  
  1237. // 2) Detection of meta tag
  1238. // This is fastest but not reliable. It is somehow a bug that the navigation might not update the meta tag content
  1239.  
  1240. // 3) Detection of HTMLElement inside video player for live video
  1241.  
  1242. // (1)+(3) = solution
  1243.  
  1244. let attr_chatblock = null
  1245. let attr_chatcollapsed = null;
  1246.  
  1247. const elmChat = document.querySelector('ytd-live-chat-frame#chat')
  1248. let elmCont = null;
  1249. if (elmChat) {
  1250. elmCont = chatFrameElement('yt-live-chat-renderer #continuations')
  1251.  
  1252.  
  1253. let s = 0;
  1254. if (elmCont) {
  1255. //not found if it is collasped.
  1256. s |= elmCont.querySelector('yt-timed-continuation') ? 1 : 0;
  1257. s |= elmCont.querySelector('yt-live-chat-replay-continuation, yt-player-seek-continuation') ? 2 : 0;
  1258. //s |= elmCont.querySelector('yt-live-chat-restricted-participation-renderer')?4:0;
  1259. if (s == 1) {
  1260. attr_chatblock = 'chat-live';
  1261. }else if (s == 2) attr_chatblock = 'chat-playback';
  1262.  
  1263. if (s == 1) $("span#tab3-txt-loader").text('');
  1264.  
  1265. } else if (!ytdFlexyElm.hasAttribute('userscript-chatblock')) {
  1266. // live chat frame but type not known
  1267. attr_chatblock = '';
  1268.  
  1269. }
  1270. //keep unknown as original
  1271. let isCollapsed = !!elmChat.hasAttribute('collapsed');
  1272. attr_chatcollapsed = isCollapsed;
  1273.  
  1274. } else {
  1275. attr_chatblock = false;
  1276. attr_chatcollapsed = false;
  1277.  
  1278. }
  1279.  
  1280. return { attr_chatblock, attr_chatcollapsed }
  1281.  
  1282. }
  1283.  
  1284.  
  1285. function mtf_ChatExist() {
  1286.  
  1287. let ytdFlexyElm = kRef(ytdFlexy);
  1288. if (!scriptEnable || !ytdFlexyElm) return;
  1289.  
  1290. if (deferredVarYTDHidden) return;
  1291.  
  1292. const elmChat = document.querySelector('ytd-live-chat-frame#chat')
  1293. let elmCont = null;
  1294. if (elmChat) {
  1295. elmCont = chatFrameElement('yt-live-chat-renderer #continuations')
  1296. }
  1297.  
  1298. const chatBlockR = (elmChat ? 1 : 0) + (elmCont ? 2 : 0)
  1299. if (Q.mtf_chatBlockQ !== chatBlockR) {
  1300. Q.mtf_chatBlockQ = chatBlockR
  1301. let rChatExist = base_ChatExist();
  1302. //console.log(2446, rChatExist)
  1303. if (rChatExist) {
  1304. let { attr_chatblock, attr_chatcollapsed } = rChatExist;
  1305. wAttr(ytdFlexyElm, 'userscript-chatblock', attr_chatblock)
  1306. wAttr(ytdFlexyElm, 'userscript-chat-collapsed', attr_chatcollapsed)
  1307. if(attr_chatblock=='chat-live') _disableComments();
  1308. }
  1309. }
  1310.  
  1311. }
  1312.  
  1313.  
  1314.  
  1315.  
  1316. let lastScrollAt1 = 0;
  1317.  
  1318. function makeBodyScrollByEvt() {
  1319. let ct = Date.now();
  1320. if (ct - lastScrollAt1 < 6) return; // avoid duplicate calling
  1321. lastScrollAt1 = ct;
  1322. window.dispatchEvent(new Event("scroll")); // dispatch Scroll Event to Window for content display
  1323. }
  1324.  
  1325. let lastScrollAt2 = 0;
  1326.  
  1327. function makeBodyScroll() {
  1328. let ct = Date.now();
  1329. if (ct - lastScrollAt2 < 30) return; // avoid over triggering
  1330. lastScrollAt2 = ct;
  1331. requestAnimationFrame(() => {
  1332. window.dispatchEvent(new Event("scroll")); // ask youtube to display content
  1333. })
  1334. }
  1335.  
  1336. //let requestingComments = null
  1337. //let scrollForComments_lastStart = 0;
  1338. /*
  1339. function scrollForComments_TF() {
  1340. let comments = requestingComments;
  1341. if (!comments) return;
  1342. if (comments.hasAttribute('hidden')) {
  1343. window.dispatchEvent(new Event("scroll"));
  1344. } else requestingComments = null;
  1345.  
  1346. }
  1347. function scrollForComments() {
  1348. scrollForComments_TF();
  1349. if (!requestingComments) return;
  1350. requestAnimationFrame(scrollForComments_TF);
  1351. let ct = Date.now();
  1352. if (ct - scrollForComments_lastStart < 60) return;
  1353. scrollForComments_lastStart = ct;
  1354. timeline.setTimeout(scrollForComments_TF, 80);
  1355. timeline.setTimeout(scrollForComments_TF, 240);
  1356. timeline.setTimeout(scrollForComments_TF, 870);
  1357. }
  1358. */
  1359.  
  1360.  
  1361.  
  1362. const mtoCs = { mtoNav: null, mtoBody: null };
  1363.  
  1364.  
  1365. const mtoVs = {}
  1366.  
  1367. const mutation_target_id_list = ['ytp-caption-window-container', 'items', 'button', 'movie_player', 'player-ads', 'hover-overlays', 'replies'];
  1368. const mutation_target_class_list = ['ytp-panel-menu', 'ytp-endscreen-content'];
  1369.  
  1370. function isMtoOverallSkip(dTarget) {
  1371.  
  1372. if (!dTarget || dTarget.nodeType !== 1) return true;
  1373.  
  1374. if (mutation_target_id_list.includes(dTarget.id)) return true;
  1375.  
  1376. for (const c of dTarget.classList) {
  1377. if (mutation_target_class_list.includes(c)) return true;
  1378. }
  1379.  
  1380. return false;
  1381. }
  1382.  
  1383.  
  1384. const mutation_div_id_ignorelist = [
  1385. 'metadata-line',
  1386. 'ytp-caption-window-container',
  1387. 'top-level-buttons-computed',
  1388. 'microformat',
  1389. 'visibility-button',
  1390. 'info-strings',
  1391. 'action-menu',
  1392. 'reply-button-end'
  1393. ];
  1394.  
  1395. const mutation_div_class_ignorelist = [
  1396. 'badge', 'tp-yt-paper-tooltip', 'ytp-autonav-endscreen-upnext-header',
  1397. 'ytp-bound-time-left', 'ytp-bound-time-right', 'ytp-share-icon',
  1398. 'ytp-tooltip-title', 'annotation', 'ytp-copylink-icon', 'ytd-thumbnail',
  1399. 'paper-ripple',
  1400. //caption
  1401. 'captions-text', 'caption-visual-line', 'ytp-caption-segment', 'ytp-caption-window-container',
  1402. //menu
  1403. 'ytp-playlist-menu-button-text',
  1404.  
  1405. 'ytp-bezel-icon', 'ytp-bezel-text',
  1406. 'dropdown-content',
  1407. 'tp-yt-paper-menu-button', 'tp-yt-iron-dropdown',
  1408.  
  1409. 'ytd-metadata-row-renderer', // #content.ytd-metadata-row-renderer inside each of ytd-metadata-row-renderer (ytd-expander)
  1410. 'ytd-engagement-panel-section-list-renderer', // {div#content.style-scope.ytd-engagement-panel-section-list-renderer} inside each of ytd-engagement-panel-section-list-renderer
  1411.  
  1412. 'autocomplete-suggestions' // autocomplete-suggestions
  1413. ];
  1414.  
  1415. const mutation_target_tag_ignorelist = [
  1416. 'ytd-channel-name', 'tp-yt-iron-dropdown', 'tp-yt-paper-tooltip',
  1417. 'tp-yt-paper-listbox', 'yt-img-shadow', 'ytd-thumbnail', 'ytd-video-meta-block',
  1418.  
  1419. 'yt-icon-button', 'tp-yt-paper-button', 'yt-formatted-string', 'yt-icon', 'button', 'paper-ripple',
  1420.  
  1421. 'ytd-player-microformat-renderer',
  1422. 'ytd-engagement-panel-section-list-renderer', 'ytd-engagement-panel-title-header-renderer',
  1423. 'ytd-comment-renderer', 'ytd-menu-renderer', 'ytd-badge-supported-renderer',
  1424. 'ytd-subscribe-button-renderer', 'ytd-subscription-notification-toggle-button-renderer',
  1425. 'ytd-button-renderer', 'ytd-toggle-button-renderer',
  1426. 'yt-pdg-comment-chip-renderer', 'ytd-comment-action-buttons-renderer', 'ytd-comment-thread-renderer',
  1427. 'ytd-compact-radio-renderer', 'ytd-compact-video-renderer',
  1428. 'ytd-video-owner-renderer',
  1429. 'ytd-metadata-row-renderer', //ytd-metadata-row-renderer is part of the #collapsible inside ytd-expander
  1430.  
  1431. 'ytd-moving-thumbnail-renderer',
  1432. 'ytd-thumbnail-overlay-toggle-button-renderer',
  1433. 'ytd-thumbnail-overlay-bottom-panel-renderer', 'ytd-thumbnail-overlay-equalizer',
  1434. 'ytd-thumbnail-overlay-now-playing-renderer', 'ytd-thumbnail-overlay-resume-playback-renderer',
  1435. 'ytd-thumbnail-overlay-side-panel-renderer', 'ytd-thumbnail-overlay-time-status-renderer',
  1436. 'ytd-thumbnail-overlay-hover-text-renderer',
  1437.  
  1438. 'yt-interaction',
  1439. 'tp-yt-paper-spinner-lite', 'tp-yt-paper-spinner',
  1440.  
  1441. 'h1', 'h2', 'h3', 'h4', 'h5', 'span', 'a',
  1442.  
  1443. 'meta', 'br', 'script', 'style', 'link', 'dom-module', 'template'
  1444. ];
  1445.  
  1446. function isMtoTargetSkip(mutation) {
  1447. //skip not important mutation tartget
  1448.  
  1449. if (!mutation) return true;
  1450. let { type, target } = mutation
  1451.  
  1452. if (!target || target.nodeType !== 1 || type != 'childList') return true;
  1453.  
  1454. let tagName = target.nodeName.toLowerCase();
  1455.  
  1456. if (mutation_target_tag_ignorelist.includes(tagName)) return true;
  1457.  
  1458. switch (tagName) {
  1459.  
  1460.  
  1461. case 'ytd-expander':
  1462. if (target.id == 'expander' && Q.comments_section_loaded == 1 && target.classList.contains('ytd-comment-renderer')) return true; // load comments
  1463. return false;
  1464.  
  1465. case 'div':
  1466.  
  1467. if (target.id == 'contents') {
  1468. return false;
  1469. }
  1470. if (mutation_div_id_ignorelist.includes(target.id)) return true;
  1471.  
  1472. for (const c of target.classList) {
  1473. if (mutation_div_class_ignorelist.includes(c)) return true;
  1474. }
  1475.  
  1476. return false;
  1477.  
  1478. }
  1479.  
  1480. return false;
  1481.  
  1482. }
  1483.  
  1484.  
  1485. function mtf_forceCheckLiveVideo() {
  1486. // once per $$player-playback-timestamp$$ {#ytd-player .ytp-time-display} && $$chat-frame$$ {ytd-live-chat-frame#chat} detection
  1487. // reset after popstatechange / videochange
  1488.  
  1489. if(mtf_forceCheckLiveVideo_disable) return ;
  1490.  
  1491. let ytdFlexyElm = kRef(ytdFlexy);
  1492. if (!scriptEnable || !ytdFlexyElm) return ;
  1493.  
  1494. const playerLabel = document.querySelector('ytd-watch-flexy:not([hidden]) #ytd-player .ytp-time-display') && document.querySelector('ytd-watch-flexy:not([hidden]) ytd-live-chat-frame#chat')
  1495. if (!playerLabel) return ;
  1496. mtf_forceCheckLiveVideo_disable = 1;
  1497. timeline.setTimeout(FP.fireOnce_forceCheckLiveVideo_tf, 170)
  1498. return ;
  1499. }
  1500.  
  1501. // continuous check for element relocation
  1502. function mtf_append_comments() {
  1503. /** @type {HTMLElement | null} */
  1504. let ytdFlexyElm = kRef(ytdFlexy);
  1505. if (!scriptEnable || !ytdFlexyElm) return;
  1506.  
  1507. /** @type {HTMLElement | null} */
  1508. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1509.  
  1510. let comments = rootElement.querySelector('#primary ytd-watch-metadata ~ ytd-comments#comments');
  1511. if (comments) $(comments).appendTo('#tab-comments').attr('data-dom-changed-by-tabview-youtube', scriptVersionForExternal)
  1512. }
  1513.  
  1514. // continuous check for element relocation
  1515. function mtf_liveChatBtnF() {
  1516. /** @type {HTMLElement | null} */
  1517. let ytdFlexyElm = kRef(ytdFlexy);
  1518. if (!scriptEnable || !ytdFlexyElm) return;
  1519.  
  1520. /** @type {HTMLElement | null} */
  1521. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1522.  
  1523. let button = rootElement.querySelector('ytd-live-chat-frame#chat > .ytd-live-chat-frame#show-hide-button:nth-child(n+2)');
  1524. if (button) button.parentNode.insertBefore(button, button.parentNode.firstChild)
  1525. }
  1526.  
  1527.  
  1528.  
  1529. // continuous check for element relocation
  1530. // fired at begining & window resize, etc
  1531. function mtf_append_playlist() {
  1532.  
  1533. /** @type {HTMLElement | null} */
  1534. let ytdFlexyElm = kRef(ytdFlexy);
  1535. if (!scriptEnable || !ytdFlexyElm) return;
  1536.  
  1537. /** @type {HTMLElement | null} */
  1538. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1539.  
  1540. let ple1 = rootElement.querySelector("*:not(#ytd-userscript-playlist) > ytd-playlist-panel-renderer#playlist");
  1541. if (ple1) {
  1542. let ct = Date.now();
  1543. let truePlaylist = null;
  1544. let truePlaylist_items = document.querySelector('ytd-playlist-panel-renderer#playlist #items:not(:empty)');
  1545. if (truePlaylist_items) {
  1546.  
  1547. let pElm = truePlaylist_items.parentNode;
  1548. while (pElm && pElm.nodeType === 1) {
  1549. if (pElm.id == 'playlist') {
  1550. pElm.setAttribute('tabview-true-playlist', ct)
  1551. truePlaylist = pElm;
  1552. break;
  1553. }
  1554. pElm = pElm.parentNode;
  1555. }
  1556.  
  1557. }
  1558.  
  1559. if (!truePlaylist) truePlaylist = ple1; // NOT NULL
  1560.  
  1561. for (const s of document.querySelectorAll(`*:not(#ytd-userscript-playlist) > ytd-playlist-panel-renderer#playlist:not([tabview-true-playlist="${ct}"])`))
  1562. s.parentNode.removeChild(s);
  1563.  
  1564. let $wrapper = getWrapper('ytd-userscript-playlist')
  1565. $wrapper.append(truePlaylist).appendTo(document.querySelector("#tab-list"));
  1566. truePlaylist.setAttribute('data-dom-changed-by-tabview-youtube', scriptVersionForExternal)
  1567. setDisplayedPlaylist(); // relocation after re-layout
  1568.  
  1569. requestAnimationFrame(() => {
  1570. let ytdFlexyElm = kRef(ytdFlexy);
  1571. if (!scriptEnable || !ytdFlexyElm) return;
  1572. if (!switchTabActivity_lastTab && (ytdFlexyElm.getAttribute('tabview-selection') + '').indexOf('#tab-') === 0 && /https\:\/\/www\.youtube\.com\/watch.*[\?\&]list=[\w\-\_]+/.test(location.href)) {
  1573. if (setToActiveTab('#tab-list')) switchTabActivity_lastTab = '#tab-list';
  1574. }
  1575. })
  1576.  
  1577. }
  1578. }
  1579.  
  1580.  
  1581. // content fix - info & playlist
  1582. // fired at begining, and keep for in case any change
  1583. function mtf_fix_details() {
  1584.  
  1585. if (!scriptEnable) return;
  1586.  
  1587. if (no_fix_contents_until < Date.now()) {
  1588. const content = document.querySelector('#meta-contents ytd-expander > #content, #tab-info ytd-expander > #content')
  1589. if (content) {
  1590. no_fix_contents_until = Date.now() + 3000;
  1591. timeline.setTimeout(function() {
  1592. const expander = content.parentNode;
  1593.  
  1594. if (expander.hasAttribute('collapsed')) wAttr(expander, 'collapsed', false);
  1595. expander.style.setProperty('--ytd-expander-collapsed-height', '');
  1596.  
  1597. let btn1 = expander.querySelector('tp-yt-paper-button#less:not([hidden])');
  1598. let btn2 = expander.querySelector('tp-yt-paper-button#more:not([hidden])');
  1599.  
  1600. if (btn1) wAttr(btn1, 'hidden', true);
  1601. if (btn2) wAttr(btn2, 'hidden', true);
  1602. }, 40);
  1603.  
  1604. }
  1605. }
  1606.  
  1607. if (no_fix_playlist_until < Date.now()) {
  1608. // just in case the playlist is collapsed
  1609. const playlist = document.querySelector('#tab-list ytd-playlist-panel-renderer#playlist')
  1610. if (playlist) {
  1611. no_fix_playlist_until = Date.now() + 3000;
  1612. timeline.setTimeout(function() {
  1613. if (playlist.hasAttribute('collapsed')) wAttr(playlist, 'collapsed', false);
  1614. if (playlist.hasAttribute('collapsible')) wAttr(playlist, 'collapsible', false);
  1615. }, 40)
  1616. }
  1617. }
  1618.  
  1619.  
  1620. }
  1621.  
  1622.  
  1623.  
  1624. function isNullComments() {
  1625.  
  1626. let comments = document.querySelector('ytd-comments#comments')
  1627. if (!comments || comments.hasAttribute('hidden')) return true;
  1628.  
  1629. }
  1630.  
  1631. function _innerCommentsLoader( /** @type {HTMLElement} */ rootElement) {
  1632.  
  1633. /** @type {HTMLElement | null} */
  1634. let ytdFlexyElm = kRef(ytdFlexy);
  1635. if (!scriptEnable || !ytdFlexyElm) return;
  1636. if (deferredVarYTDHidden) return;
  1637.  
  1638.  
  1639. let messageElm, messageStr, commentRenderer;
  1640. let diffCSS = `[tabview-cache-time="${sect_hTime}"]`
  1641.  
  1642. if (commentRenderer = rootElement.querySelector(`ytd-comments#comments #count.ytd-comments-header-renderer:not(${diffCSS})`)) {
  1643.  
  1644. scheduledCommentRefresh=false;
  1645. let eTime = Date.now();
  1646.  
  1647. sect_holder = mWeakRef(commentRenderer);
  1648. sect_hText = commentRenderer.textContent;
  1649. sect_hTime = eTime;
  1650. commentRenderer.setAttribute('tabview-cache-time', eTime);
  1651.  
  1652. return {
  1653. status: 1,
  1654. f: () => {
  1655.  
  1656. let span = document.querySelector("span#tab3-txt-loader")
  1657. let r = '0';
  1658. let txt = commentRenderer.textContent
  1659. if (typeof txt == 'string') {
  1660. let m = txt.match(/[\d\,\s]+/)
  1661. if (m) {
  1662. r = m[0].trim()
  1663.  
  1664.  
  1665. }
  1666. }
  1667. if (span){
  1668.  
  1669. let tab_btn = span.closest('.tab-btn[userscript-tab-content="#tab-comments"]')
  1670. if(tab_btn)tab_btn.setAttribute('loaded-comment','normal')
  1671. span.textContent = r;
  1672. }
  1673. //console.log(754)
  1674. mtoInterval = mtoInterval2;
  1675. scheduledCommentRefresh=false;
  1676. setCommentSection(1);
  1677. comments_section_loaded_elm = mWeakRef(document.querySelector('ytd-comments#comments div#contents'))
  1678. }
  1679. }
  1680.  
  1681. } else if ((messageElm = rootElement.querySelector(`ytd-item-section-renderer#sections #header ~ #contents > ytd-message-renderer:not(${diffCSS})`)) && (messageStr = (messageElm.textContent || '').trim())) { //ytd-message-renderer
  1682. // it is possible to get the message before the header generation.
  1683. // sample link - https://www.youtube.com/watch?v=PVUZ8Nvr1ic
  1684. // sample link - https://www.youtube.com/watch?v=yz8AiQc1Bk8
  1685.  
  1686. scheduledCommentRefresh=false;
  1687. let eTime = Date.now();
  1688.  
  1689. sect_holder = mWeakRef(messageElm);
  1690. sect_hText = messageElm.textContent;
  1691. sect_hTime = eTime;
  1692. messageElm.setAttribute('tabview-cache-time', eTime);
  1693.  
  1694. return {
  1695. status: 2,
  1696. f: () => {
  1697. timeline.setTimeout(function() {
  1698. let span = document.querySelector("span#tab3-txt-loader")
  1699. const mainMsg = messageElm.querySelector('#message, #submessage')
  1700. if (mainMsg && mainMsg.textContent) {
  1701. for (const msg of mainMsg.querySelectorAll('*:not(:empty)')) {
  1702. if (msg.childElementCount === 0 && msg.textContent) {
  1703. messageStr = msg.textContent.trim()
  1704. break
  1705. }
  1706. }
  1707. }
  1708. if (span){
  1709. let tab_btn = span.closest('.tab-btn[userscript-tab-content="#tab-comments"]')
  1710. if(tab_btn)tab_btn.setAttribute('loaded-comment','message')
  1711. span.textContent ='\u200B';
  1712. }
  1713. mtoInterval = mtoInterval2;
  1714. scheduledCommentRefresh=false;
  1715. setCommentSection(1);
  1716. comments_section_loaded_elm = mWeakRef(document.querySelector('ytd-comments#comments div#contents'))
  1717. }, 40);
  1718. }
  1719. }
  1720.  
  1721. }
  1722.  
  1723. }
  1724.  
  1725. const atChange = async ()=>{
  1726.  
  1727. // call when video change / popstate change / switch back from mini-view
  1728.  
  1729. let ytdFlexyElm = kRef(ytdFlexy);
  1730. if(!ytdFlexyElm) return;
  1731.  
  1732. mtf_forceCheckLiveVideo_disable = 0;
  1733. mtf_forceCheckLiveVideo();
  1734.  
  1735. if(Q.comments_section_loaded===0){
  1736.  
  1737. const tabBtn = document.querySelector('[userscript-tab-content="#tab-comments"].tab-btn-hidden')
  1738. if(tabBtn) {
  1739. tabBtn.classList.remove("tab-btn-hidden") //if contains
  1740. }
  1741.  
  1742. }
  1743. }
  1744.  
  1745.  
  1746.  
  1747. function hookSection(){
  1748.  
  1749.  
  1750. //console.log(3300)
  1751.  
  1752. let ytdFlexyElm = kRef(ytdFlexy);
  1753. if (!scriptEnable || !ytdFlexyElm) return ;
  1754.  
  1755. const comments = ytdFlexyElm.querySelector('ytd-comments#comments')
  1756. if (!comments) return ;
  1757.  
  1758. if(deferredVarYTDHidden) return;
  1759.  
  1760. //console.log(3310)
  1761.  
  1762. //new MutationObserver(function(){console.log(comments.querySelectorAll('*').length)}).observe(comments,{ attributes: true, childList: true, subtree: true })
  1763.  
  1764. function foundSection(sections){
  1765.  
  1766. //console.log(3320)
  1767.  
  1768. function ulfx() {
  1769. //console.log(1220)
  1770.  
  1771.  
  1772. const rootElement = comments.parentNode
  1773.  
  1774.  
  1775. if (pendingOne && comments.querySelectorAll('[tabview-cache-time]').length > 1) return;
  1776. pendingOne = false;
  1777. if(!rootElement) return; //prevent unknown condition
  1778.  
  1779.  
  1780. let innerCommentsLoaderRet = _innerCommentsLoader(rootElement);
  1781.  
  1782. //console.log(1222,_innerCommentsLoader)
  1783.  
  1784. if (!innerCommentsLoaderRet) {
  1785.  
  1786.  
  1787. let holder = kRef(sect_holder);
  1788. if (holder && comments.contains(holder)) {
  1789.  
  1790. if (sect_lastUpdate) return;
  1791. sect_lastUpdate = 1;
  1792.  
  1793. timeline.setTimeout(function() {
  1794. //console.log(2633)
  1795. if (holder.textContent !== sect_hText && sect_hTime > 0) {
  1796.  
  1797. if (comments.querySelectorAll('[tabview-cache-time]').length === 1) {
  1798.  
  1799. sect_hTime--;
  1800.  
  1801. let innerCommentsLoaderRet = _innerCommentsLoader(rootElement);
  1802.  
  1803. if (innerCommentsLoaderRet) {
  1804. //console.log(9442)
  1805. innerCommentsLoaderRet.f();
  1806. fetchCommentsFinished();
  1807. }
  1808.  
  1809. pendingOne = false;
  1810.  
  1811. }
  1812.  
  1813.  
  1814. }
  1815. sect_lastUpdate = 0
  1816. }, 400)
  1817.  
  1818. } else if (sect_holder) {
  1819. sect_holder = null
  1820. sect_hTime = 0;
  1821. sect_lastUpdate = 0;
  1822. pendingOne = false;
  1823. timeline.setTimeout(()=>{
  1824. if(!deferredVarYTDHidden && scheduledCommentRefresh && Q.comments_section_loaded>0 && comments.hasAttribute('hidden')){
  1825.  
  1826. scheduledCommentRefresh=false; // logic tbc
  1827. resetCommentSection();
  1828. }
  1829. },142);
  1830. Q.mutationTarget=null;
  1831. FP.mtf_attrComments(); // in case for popstate
  1832. }
  1833.  
  1834.  
  1835. } else {
  1836.  
  1837. //console.log(9443)
  1838. innerCommentsLoaderRet.f();
  1839. fetchCommentsFinished();
  1840.  
  1841. pendingOne = true;
  1842.  
  1843. }
  1844.  
  1845.  
  1846.  
  1847. }
  1848.  
  1849. function observerHook(mutationsList, observer){
  1850.  
  1851. let valid = false
  1852. for (const mutation of mutationsList) {
  1853.  
  1854. let target = mutation.target;
  1855. if (!target) break;
  1856. if (!target.id) break;
  1857. let classList = target.classList;
  1858.  
  1859.  
  1860. if (
  1861. classList.contains('ytd-item-section-renderer') ||
  1862. classList.contains('ytd-comments-header-renderer') ||
  1863. classList.contains('ytd-message-renderer')) {
  1864. valid = true
  1865. break;
  1866. }
  1867.  
  1868. break; // only outest mutation
  1869.  
  1870.  
  1871. }
  1872. if (valid) ulfx();
  1873. }
  1874.  
  1875. if(sections.hasAttribute('tabview-comment-section-checked')){
  1876.  
  1877. //console.log(3612)
  1878. ulfx();
  1879.  
  1880. }else{
  1881.  
  1882. pendingOne = false;
  1883. //console.log(3611)
  1884. const observer = new MutationObserver(observerHook)
  1885. sections.setAttribute('tabview-comment-section-checked','')
  1886. const config = { childList: true, subtree: true };
  1887. observer.observe(sections, config);
  1888. ulfx();
  1889.  
  1890.  
  1891. }
  1892.  
  1893.  
  1894.  
  1895. }
  1896. //console.log(3241, location.href)
  1897. let cid_render_section = timeline.setInterval(function() {
  1898.  
  1899. // console.log(3244, location.href)
  1900.  
  1901.  
  1902. const rootElement = comments.parentNode
  1903.  
  1904. let sections = rootElement.querySelector('ytd-comments#comments > ytd-item-section-renderer#sections');
  1905.  
  1906.  
  1907. if (sections) {
  1908.  
  1909. timeline.clearInterval(cid_render_section);
  1910. foundSection(sections);
  1911.  
  1912. }
  1913.  
  1914. }, 30)
  1915.  
  1916.  
  1917.  
  1918. }
  1919.  
  1920.  
  1921. let mtc_store=0;
  1922. let mtc_cid = 0;
  1923. const cssOnceFunc = {
  1924. [`ytd-comments#comments:not([o3r-${sa_comments}])`]:(comments)=>{
  1925. // once per {ytd-comments#comments} detection
  1926.  
  1927. let ytdFlexyElm = kRef(ytdFlexy);
  1928. if (!scriptEnable || !ytdFlexyElm) return;
  1929.  
  1930. if (!comments) return;
  1931.  
  1932.  
  1933.  
  1934.  
  1935. if(mtoVisibility_Comments.bindElement(comments)){
  1936. mtoVisibility_Comments.observer.check(9);
  1937. }
  1938. pageInit_attrComments = true
  1939. hookSection();
  1940.  
  1941. //let dComments = comments
  1942. //timeline.setTimeout(() => nativeFunc(dComments, "loadComments"), 20)
  1943. //comments = null;
  1944.  
  1945. },
  1946. [`ytd-playlist-panel-renderer#playlist:not([o3r-${sa_playlist}])`]:(playlist)=>{
  1947.  
  1948. // once per {ytd-playlist-panel-renderer#playlist} detection
  1949.  
  1950. let ytdFlexyElm = kRef(ytdFlexy);
  1951. if (!scriptEnable || !ytdFlexyElm) return;
  1952.  
  1953. if (!playlist) return;
  1954.  
  1955. if(mtoVisibility_Playlist.bindElement(playlist)){
  1956. mtoVisibility_Playlist.observer.check(9) //delay check required for browser bug - hidden changed not triggered
  1957. }
  1958. playlist = null;
  1959.  
  1960. return;
  1961.  
  1962. },
  1963. '#meta-contents ytd-expander:not([tabview-info-expander])':(expander)=>{
  1964.  
  1965. // once per $$native-info-description$$ {#meta-contents ytd-expander} detection
  1966.  
  1967. let ytdFlexyElm = kRef(ytdFlexy);
  1968. if (!scriptEnable || !ytdFlexyElm) return ;
  1969.  
  1970. if (!expander) return ;
  1971. expander.setAttribute('tabview-info-expander','')
  1972. $(expander).appendTo("#tab-info").attr('data-dom-changed-by-tabview-youtube', scriptVersionForExternal)
  1973.  
  1974. },
  1975. '#description-and-actions.style-scope.ytd-watch-metadata > #description ytd-text-inline-expander:not([tabview-removed-duplicate])': (teaserInfo)=>{
  1976.  
  1977.  
  1978. // for Teaser UI
  1979. // once per {#description-and-actions.style-scope.ytd-watch-metadata > #description > ytd-text-inline-expander} detection
  1980.  
  1981. let ytdFlexyElm = kRef(ytdFlexy);
  1982. if (!scriptEnable || !ytdFlexyElm) return ;
  1983. let addedInfo = document.querySelector('#tab-info ytd-expander[tabview-info-expander]');
  1984.  
  1985. if(!addedInfo) return;
  1986.  
  1987. if(!document.documentElement.hasAttribute('tabview-injection-js-1-ready')) return;
  1988.  
  1989. teaserInfo.setAttribute('tabview-removed-duplicate','')
  1990.  
  1991.  
  1992. teaserInfo.dispatchEvent(new CustomEvent('tabview-no-duplicate-info'))
  1993. //console.log(56)
  1994.  
  1995. }
  1996. }
  1997. const cssOnce = Object.keys(cssOnceFunc).join(', ')
  1998.  
  1999. const regularCheck = (addP, removeP, mutationTarget)=>{
  2000.  
  2001.  
  2002. let isInvalidAdding = mutationTarget && !mutationTarget.parentNode
  2003.  
  2004. let promisesForAddition = !scriptEnable ? [] : addP > 0 && !isInvalidAdding ? [
  2005.  
  2006. (async () => {
  2007.  
  2008. //$callOnce('mtf_checkFlexy');
  2009.  
  2010. const rootElement = Q.mutationTarget || kRef(ytdFlexy);
  2011.  
  2012. if(rootElement){
  2013.  
  2014. let fElms = rootElement.querySelectorAll(cssOnce)
  2015. if(fElms.length>0){
  2016.  
  2017. for(const fElm of fElms){
  2018. for(let cssSelctor in cssOnceFunc){
  2019. if(fElm.matches(cssSelctor)){
  2020. let f= cssOnceFunc[cssSelctor]
  2021. if(typeof f=='function') f(fElm);
  2022. break;
  2023. }
  2024.  
  2025. }
  2026. }
  2027.  
  2028. //$callOnce('mtf_initalAttr_comments');
  2029. //$callOnce('mtf_initalAttr_playlist');
  2030. // $callOnce('mtf_checkDescriptionLoaded');
  2031. }
  2032.  
  2033. }
  2034.  
  2035. })(),
  2036. (async () => {
  2037.  
  2038.  
  2039. FP.mtf_initalAttr_chatroom();
  2040. FP.mtf_initalAttr_engagement_panel();
  2041. //$callOnce('mtf_initalAttr_chatroom');
  2042. // $callOnce('mtf_initalAttr_engagement_panel');
  2043.  
  2044. mtf_forceCheckLiveVideo();
  2045. mtf_append_comments();
  2046. mtf_liveChatBtnF();
  2047. fixTabs();
  2048. mtf_AfterFixTabs();
  2049. mtf_append_playlist();
  2050.  
  2051. })()
  2052.  
  2053. ] : [];
  2054. let promisesForEveryMutation = !scriptEnable ? [] : [
  2055. (async () => {
  2056. mtf_fix_details();
  2057. mtf_ChatExist();
  2058. })()
  2059. ];
  2060.  
  2061. return [...promisesForAddition, ...promisesForEveryMutation]
  2062.  
  2063.  
  2064. }
  2065.  
  2066. function setOnlyOneEPanel(ePanel){
  2067.  
  2068. layoutStatusMutex.lockWith(unlock => {
  2069.  
  2070. let cPanels = engagement_panels_();
  2071. for (const entry of cPanels.list) {
  2072. if (entry.ePanel != ePanel && entry.visible) ytBtnCloseEngagementPanel(entry.ePanel);
  2073. }
  2074. setTimeout(unlock, 30)
  2075.  
  2076. })
  2077.  
  2078. }
  2079.  
  2080. const FP = {
  2081.  
  2082. mtoNav_delayedF: () => {
  2083.  
  2084. let { addP, removeP, mutationTarget } = Q;
  2085.  
  2086. Q.addP = 0;
  2087. Q.removeP = 0;
  2088. mtc_store=Date.now()+1870
  2089.  
  2090.  
  2091. let regularChecks = regularCheck(addP, removeP, mutationTarget);
  2092.  
  2093. Promise.all(regularChecks).then(() => {
  2094. regularChecks = null;
  2095. mtc_store= Date.now() + 3870 // pending Comments start to load
  2096. Q.mutationTarget = null;
  2097.  
  2098. Q.mtoNav_requestNo--;
  2099. //console.log('motnav reduced to', mtoNav_requestNo)
  2100. if (Q.mtoNav_requestNo > 0) {
  2101. Q.mtoNav_requestNo = 1;
  2102. setTimeout(FP.mtoNav_delayedF, mtoInterval);
  2103. }
  2104. })
  2105.  
  2106.  
  2107. },
  2108.  
  2109. mtoNavF: ( /** @type {MutationRecord[]} */ mutations, /** @type {MutationObserver} */ observer) => {
  2110. //subtree DOM mutation checker - {ytd-watch-flexy} \single \subtree
  2111.  
  2112. newVideoPageCheck()
  2113.  
  2114. if (!scriptEnable) return;
  2115. if (deferredVarYTDHidden) return;
  2116.  
  2117. let ch = false;
  2118.  
  2119. let reg = [];
  2120. let dTarget = null;
  2121.  
  2122. let wAddP = 0,
  2123. wRemoveP = 0;
  2124.  
  2125. let _last_mto_target = null;
  2126. let _last_mto_target_valid = null;
  2127.  
  2128. for (const mutation of mutations) {
  2129. if (!mutation || !mutation.target || !mutation.target.parentNode) continue;
  2130.  
  2131. let elementalMutation = false;
  2132. let tAddP = 0,
  2133. tRemoveP = 0;
  2134.  
  2135. for (const addedNode of mutation.addedNodes) { //theoretically faster: only reading of states
  2136. if (addedNode.nodeType === 1) {
  2137. tAddP++;
  2138. elementalMutation = true;
  2139. }
  2140. }
  2141.  
  2142. for (const removedNode of mutation.removedNodes) { //theoretically faster: only reading of states
  2143. if (removedNode.nodeType === 1) {
  2144. tRemoveP++;
  2145. elementalMutation = true;
  2146. }
  2147. }
  2148.  
  2149. if (elementalMutation) { //skip all addition and removal operations without elemental changes (e.g. textNode modification)
  2150.  
  2151. if (_last_mto_target === mutation.target) {
  2152. // due to addition and removal operations to the same DOM
  2153. if (_last_mto_target_valid) {
  2154. // AddP & RemoveP is still valid
  2155. wAddP += tAddP;
  2156. wRemoveP += tRemoveP;
  2157. }
  2158. continue;
  2159. }
  2160. _last_mto_target = mutation.target;
  2161.  
  2162. if (isMtoTargetSkip(mutation)) {
  2163. _last_mto_target_valid = false;
  2164. continue; //theoretically slower: creation of string variables
  2165. } else {
  2166. _last_mto_target_valid = true;
  2167. wAddP += tAddP;
  2168. wRemoveP += tRemoveP;
  2169. }
  2170.  
  2171.  
  2172.  
  2173. ch = true;
  2174.  
  2175. reg.push(mutation);
  2176.  
  2177. if (dTarget === null) dTarget = mutation.target; //first
  2178. else if (dTarget === true) {} //ytdFlexy
  2179. else if (dTarget.contains(mutation.target)) {} //first node is the container to all subsequential targets
  2180. else { dTarget = true; } //the target is not the child of first node
  2181.  
  2182. }
  2183.  
  2184. }
  2185.  
  2186. if (!ch) return; // dTarget must be true OR HTMLElement
  2187.  
  2188. if (dTarget === true) dTarget = kRef(ytdFlexy); // major mutation occurance
  2189. else if (dTarget === kRef(comments_section_loaded_elm) && wAddP > wRemoveP) return true; // ignore if comments are loaded (adding comments)
  2190. else if (isMtoOverallSkip(dTarget)) return; // allow for multiple mutations at the same time - determinated after looping
  2191.  
  2192.  
  2193.  
  2194. // 4 ~ 16 times per full page loading
  2195.  
  2196. Q.addP += wAddP;
  2197. Q.removeP += wRemoveP;
  2198.  
  2199. if (Q.mutationTarget === null) Q.mutationTarget = dTarget;
  2200. else if (Q.mutationTarget != dTarget) Q.mutationTarget = kRef(ytdFlexy);
  2201.  
  2202. //console.log(prettyElm(dTarget), wAddP , wRemoveP, mtoInterval)
  2203. //console.log(prettyElm(dTarget), reg.map(m=>prettyElm(m.target)))
  2204. //console.log(7015, performance.now())
  2205.  
  2206.  
  2207.  
  2208. Q.mtoNav_requestNo++;
  2209. if (Q.mtoNav_requestNo == 1) setTimeout(FP.mtoNav_delayedF, mtoInterval);
  2210.  
  2211. },
  2212.  
  2213.  
  2214. mtoBodyF: ( /** @type {MutationRecord[]} */ mutations, /** @type {MutationObserver} */ observer) => {
  2215. //subtree DOM mutation checker - {body} \single \subtree
  2216.  
  2217. if (!scriptEnable) return;
  2218. if (deferredVarYTDHidden) return;
  2219.  
  2220. for (const mutation of mutations) {
  2221. for (const addedNode of mutation.addedNodes)
  2222. if (addedNode.nodeType === 1) {
  2223. if (addedNode.nodeName == "DIV" && addedNode.matches('.autocomplete-suggestion:not([autocomplete-disable-updatesc])') ) {
  2224. mtf_fixAutoCompletePosition(addedNode)
  2225. }else if(addedNode.nodeName == "DIV" && (addedNode.id==='lyricscontainer' || addedNode.id==='showlyricsbutton')){
  2226.  
  2227. goYoutubeGeniusLyrics();
  2228.  
  2229. }
  2230. }
  2231. }
  2232.  
  2233. },
  2234.  
  2235. mtf_attrPlaylist: (attrName, newValue) => {
  2236. //attr mutation checker - {ytd-playlist-panel-renderer#playlist} \single
  2237. //::attr ~ hidden
  2238. //console.log(1210)
  2239.  
  2240. if (!scriptEnable) return;
  2241. if (deferredVarYTDHidden) return;
  2242. let cssElm = kRef(ytdFlexy);
  2243. if (!cssElm) return;
  2244.  
  2245. let playlist = document.querySelector('ytd-playlist-panel-renderer#playlist')
  2246. const tabBtn = document.querySelector('[userscript-tab-content="#tab-list"]');
  2247. let isPlaylistHidden = playlist.hasAttribute('hidden')
  2248. //console.log(1212.2, isPlaylistHidden, playlist.getAttribute('hidden'))
  2249. if (tabBtn) {
  2250. //console.log('attr playlist changed')
  2251. if (tabBtn.classList.contains('tab-btn-hidden') && !isPlaylistHidden) {
  2252. //console.log('attr playlist changed - no hide')
  2253. tabBtn.classList.remove("tab-btn-hidden");
  2254. } else if (!tabBtn.classList.contains('tab-btn-hidden') && isPlaylistHidden) {
  2255. //console.log('attr playlist changed - add hide')
  2256. hideTabBtn(tabBtn);
  2257. }
  2258. }
  2259. /* visible layout for triggering hidden removal */
  2260. akAttr(cssElm, 'tabview-youtube-playlist', isPlaylistHidden);
  2261. },
  2262. mtf_attrComments: (attrName, newValue) => {
  2263. //attr mutation checker - {ytd-comments#comments} \single
  2264. //::attr ~ hidden
  2265.  
  2266. let ytdFlexyElm = kRef(ytdFlexy);
  2267. if (!scriptEnable || !ytdFlexyElm) return;
  2268. if (deferredVarYTDHidden) return;
  2269.  
  2270. let comments = document.querySelector('ytd-comments#comments')
  2271. const tabBtn = document.querySelector('[userscript-tab-content="#tab-comments"]');
  2272. if (!comments || !tabBtn) return;
  2273. let isCommentHidden = comments.hasAttribute('hidden')
  2274. //console.log('attr comments changed')
  2275.  
  2276. mtoInterval = mtoInterval1;
  2277. mtc_store=Date.now()+2870
  2278.  
  2279. if( mtf_forceCheckLiveVideo_disable === 2 ){
  2280.  
  2281. }else if (!isCommentHidden) {
  2282.  
  2283. akAttr(ytdFlexyElm, 'tabview-youtube-comments', false, 'K');
  2284.  
  2285.  
  2286.  
  2287. //console.log('attr comments changed - no hide')
  2288. tabBtn.classList.remove("tab-btn-hidden") //if contains
  2289.  
  2290. //console.log(703)
  2291.  
  2292. mtc_cid&&timeline.clearInterval(mtc_cid);
  2293.  
  2294. } else if (isCommentHidden) {
  2295.  
  2296. akAttr(ytdFlexyElm, 'tabview-youtube-comments', true, 'K');
  2297. timeline.setTimeout(function(){
  2298. if(!deferredVarYTDHidden && scheduledCommentRefresh && Q.comments_section_loaded>0 && comments.hasAttribute('hidden')){
  2299. //console.log(3434)
  2300. scheduledCommentRefresh=false; //logic tbc
  2301. resetCommentSection();
  2302. //console.log(8022)
  2303. }
  2304. },142)
  2305.  
  2306. if(!mtc_cid) mtc_cid=timeline.setInterval(()=>{
  2307. if(mtc_store>Date.now()) return;
  2308. timeline.clearInterval(mtc_cid)
  2309. mtc_cid=0;
  2310. if(mtf_forceCheckLiveVideo_disable===2)return;
  2311. if(isNullComments()) _disableComments();
  2312.  
  2313. },80)
  2314.  
  2315. }
  2316.  
  2317.  
  2318. },
  2319.  
  2320.  
  2321. mtf_attrChatroom: (attrName, newValue) => {
  2322. //attr mutation checker - {ytd-live-chat-frame#chat} \single
  2323. //::attr ~ collapsed
  2324.  
  2325. let ytdFlexyElm = kRef(ytdFlexy);
  2326. if (!scriptEnable || !ytdFlexyElm) return;
  2327. if (deferredVarYTDHidden) return;
  2328.  
  2329. layoutStatusMutex.lockWith(unlock => {
  2330.  
  2331. const chatBlock = document.querySelector('ytd-live-chat-frame#chat')
  2332. const cssElm = kRef(ytdFlexy)
  2333.  
  2334. if (!chatBlock || !cssElm) {
  2335. unlock();
  2336. return;
  2337. }
  2338.  
  2339. if (deferredVarYTDHidden) {
  2340. unlock();
  2341. return;
  2342. }
  2343.  
  2344. if (!cssElm.hasAttribute('userscript-chatblock')) wAttr(cssElm, 'userscript-chatblock', true);
  2345. let isCollapsed = !!chatBlock.hasAttribute('collapsed');
  2346. wAttr(cssElm, 'userscript-chat-collapsed', isCollapsed);
  2347.  
  2348. if (cssElm.hasAttribute('userscript-chatblock') && !isCollapsed) lastShowTab = '#chatroom';
  2349.  
  2350. if (!isCollapsed && document.querySelector('#right-tabs .tab-btn.active') && isWideScreenWithTwoColumns() && !isTheater()) {
  2351. switchTabActivity(null);
  2352. timeline.setTimeout(unlock, 40);
  2353. } else {
  2354. unlock();
  2355. }
  2356.  
  2357. if (!isCollapsed) {
  2358. runAfterExpandChat();
  2359. } else {
  2360. chatBlock.removeAttribute('yt-userscript-iframe-loaded');
  2361. }
  2362.  
  2363. })
  2364.  
  2365.  
  2366.  
  2367.  
  2368. },
  2369.  
  2370. mtf_attrEngagementPanel: ( /** @type {MutationRecord[]} */ mutations, /** @type {MutationObserver} */ observer) => {
  2371. //attr mutation checker - {ytd-engagement-panel-section-list-renderer} \mutiple
  2372. //::attr ~ visibility
  2373.  
  2374. let ytdFlexyElm = kRef(ytdFlexy);
  2375. if (!scriptEnable || !ytdFlexyElm) return;
  2376.  
  2377. //multiple instance
  2378. if (mutations) {
  2379. for (const mutation of mutations) {
  2380. let ePanel = mutation.target;
  2381. if (ePanel.getAttribute('visibility') == 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED') {
  2382. setOnlyOneEPanel(ePanel);
  2383. break;
  2384. }
  2385. }
  2386. }
  2387. if (deferredVarYTDHidden) return;
  2388.  
  2389. layoutStatusMutex.lockWith(unlock => {
  2390.  
  2391. const ePanel = document.querySelector('ytd-watch-flexy ytd-engagement-panel-section-list-renderer')
  2392. const cssElm = kRef(ytdFlexy)
  2393.  
  2394. if (!ePanel || !cssElm) {
  2395. unlock();
  2396. return;
  2397. }
  2398. let previousValue = +cssElm.getAttribute('userscript-engagement-panel') || 0;
  2399.  
  2400. let { shownRes, value, count } = engagement_panels_();
  2401. let nextValue = value;
  2402. let nextCount = count;
  2403.  
  2404.  
  2405. if (nextCount == 0 && cssElm.hasAttribute('userscript-engagement-panel')) {
  2406. storeLastPanel=null;
  2407. wAttr(cssElm, 'userscript-engagement-panel', false);
  2408. unlock();
  2409. } else {
  2410.  
  2411. if ((nextCount > 1) || (cssElm.hasAttribute('userscript-engagement-panel') && previousValue === nextValue)) {
  2412. unlock();
  2413. return;
  2414. }
  2415.  
  2416. cssElm.setAttribute('userscript-engagement-panel', nextValue);
  2417.  
  2418. let b = false;
  2419. if (previousValue != nextValue && nextValue > 0) {
  2420. lastShowTab = `#engagement-panel-${nextValue}`;
  2421. b = true;
  2422. storeLastPanel = mWeakRef( shownRes[0])
  2423. //console.log(9999, shownRes[0])
  2424. }
  2425.  
  2426. if (b && document.querySelector('#right-tabs .tab-btn.active') && isWideScreenWithTwoColumns() && !isTheater()) {
  2427. switchTabActivity(null);
  2428. timeline.setTimeout(unlock, 40);
  2429. } else {
  2430. unlock();
  2431. }
  2432. }
  2433.  
  2434. })
  2435.  
  2436.  
  2437.  
  2438.  
  2439. },
  2440.  
  2441. mtf_initalAttr_playlist: () => {
  2442. },
  2443.  
  2444. mtf_initalAttr_comments: () => {
  2445. },
  2446.  
  2447. mtf_initalAttr_chatroom: () => {
  2448. // every per [new] {ytd-live-chat-frame#chat} detection - reset after mini-playview
  2449. let ytdFlexyElm = kRef(ytdFlexy);
  2450. if (!scriptEnable || !ytdFlexyElm) return true;
  2451.  
  2452. const rootElement = Q.mutationTarget || ytdFlexyElm;
  2453.  
  2454.  
  2455. if (!mgChatFrame.inPage()) {
  2456.  
  2457. mtoVisibility_Chatroom.clear(true);
  2458.  
  2459. let chatroom = rootElement.querySelector(`ytd-live-chat-frame#chat:not([${sa_chatroom}]`)
  2460. if (chatroom) {
  2461. mgChatFrame.setVar(chatroom);
  2462.  
  2463. if(mtoVisibility_Chatroom.bindElement(chatroom)){
  2464. mtoVisibility_Chatroom.observer.check(9)
  2465. }
  2466.  
  2467. chatroom = null
  2468. }
  2469. }
  2470. return true;
  2471.  
  2472. },
  2473.  
  2474. mtf_initalAttr_engagement_panel: () => {
  2475. // every per $$non-checked-section-list$$ {ytd-watch-flexy ytd-engagement-panel-section-list-renderer:not([o3r-XXXXX])} detection
  2476. let ytdFlexyElm = kRef(ytdFlexy);
  2477. if (!scriptEnable || !ytdFlexyElm) return true;
  2478.  
  2479. const rootElement = Q.mutationTarget || ytdFlexyElm;
  2480.  
  2481. let toCheck = false;
  2482. for (const engagement_panel of rootElement.querySelectorAll(
  2483. `ytd-watch-flexy ytd-engagement-panel-section-list-renderer:not([o3r-${sa_epanel}])`
  2484. )) {
  2485.  
  2486.  
  2487.  
  2488. let bindRes = mtoVisibility_EngagementPanel.bindElement(engagement_panel, {
  2489. attributes: true,
  2490. attributeFilter: ['visibility'],
  2491. attributeOldValue: true
  2492. })
  2493.  
  2494. if(bindRes) toCheck = true;
  2495. }
  2496.  
  2497. if (toCheck) FP.mtf_attrEngagementPanel()
  2498.  
  2499. return true;
  2500. },
  2501.  
  2502.  
  2503.  
  2504. //live-chat / chat-replay
  2505.  
  2506. fireOnce_forceCheckLiveVideo_tf: () => {
  2507. // once per $$player-playback-timestamp$$ {#ytd-player .ytp-time-display} && $$chat-frame$$ {ytd-live-chat-frame#chat} detection
  2508.  
  2509. let ytdFlexyElm = kRef(ytdFlexy);
  2510. if (!scriptEnable || !ytdFlexyElm) return;
  2511.  
  2512. let elm = document.querySelector('#ytd-player .ytp-time-display');
  2513. if (elm && elm.classList.contains('ytp-live')) {
  2514. //console.log(7006)
  2515. ytdFlexyElm.setAttribute('userscript-chatblock', 'chat-live')
  2516. //console.log(2441)
  2517. mtf_forceCheckLiveVideo_disable = 2;
  2518. _disableComments();
  2519. //console.log(701)
  2520. //disableComments_LiveChat();
  2521. }
  2522. },
  2523.  
  2524.  
  2525. //comments
  2526. /*
  2527. mtf_advancedComments: () => {
  2528. // once per {ytd-comments#comments #continuations} detection
  2529.  
  2530. //let ytdFlexyElm = kRef(ytdFlexy);
  2531. //if (!scriptEnable || !ytdFlexyElm) return true;
  2532.  
  2533. //const rootElement = Q.mutationTarget || ytdFlexyElm;
  2534. //const continuations = document.querySelector("ytd-comments#comments #continuations");
  2535. //if (!continuations) return true;
  2536. //requestingComments = document.querySelector('ytd-comments#comments');
  2537. //scrollForComments();
  2538. return false;
  2539. }
  2540. */
  2541.  
  2542.  
  2543. }
  2544.  
  2545.  
  2546.  
  2547.  
  2548.  
  2549. /** @type {WeakRef | null} */
  2550. let displayedPlaylist = null;
  2551. /** @type {WeakRef | null} */
  2552. let scrollingVideosList = null;
  2553.  
  2554. let scriptEnable = false;
  2555. let scriptEC = 0;
  2556. let lastShowTab = null;
  2557.  
  2558. let _cachedLastVideo = null;
  2559. let videoListBeforeSearch = null;
  2560. let no_fix_contents_until = 0;
  2561. let no_fix_playlist_until = 0;
  2562. let statusCollasped = 0;
  2563.  
  2564. /** @type {WeakRef | null} */
  2565. let ytdFlexy = null;
  2566.  
  2567. function pluginUnhook() {
  2568. _pluginUnhook();
  2569. resetCommentSection();
  2570. }
  2571.  
  2572. function _pluginUnhook() {
  2573.  
  2574. sect_lastUpdate = 0;
  2575. sect_holder = null;
  2576. sect_hTime = 0;
  2577.  
  2578. //console.log(8001)
  2579.  
  2580. videoListBeforeSearch = null;
  2581. statusCollasped = 0;
  2582. _cachedLastVideo = null;
  2583. lastShowTab = null;
  2584. displayedPlaylist = null;
  2585. scrollingVideosList = null;
  2586. scriptEnable = false;
  2587. scriptEC++;
  2588. if (scriptEC > 788888888) scriptEC = 188888888;
  2589. ytdFlexy = null;
  2590. wls.layoutStatus = null;
  2591.  
  2592. //console.log('unc01')
  2593.  
  2594. mtoVisibility_EngagementPanel.clear(true)
  2595. mtoVisibility_Playlist.clear(true)
  2596. mtoVisibility_Comments.clear(true)
  2597.  
  2598. mgChatFrame.kVar = null;
  2599. mtoVisibility_Chatroom.clear(true)
  2600. mtoFlexyAttr.clear(true)
  2601.  
  2602.  
  2603. for (const elem of document.querySelectorAll(
  2604. ['ytd-expander[tabview-info-expander]'].join(', ')
  2605. )) {
  2606. elem.removeAttribute('tabview-info-expander');
  2607. }
  2608.  
  2609. mtoMutation_body.clear(true)
  2610. mtoMutation_watchFlexy.clear(true)
  2611. //FOnce.mtf_checkFlexy = null;
  2612. //FOnce.mtf_initalAttr_comments = null;
  2613. //FOnce.mtf_initalAttr_playlist = null;
  2614. //FOnce.mtf_initalAttr_chatroom = null;
  2615. //FOnce.mtf_initalAttr_engagement_panel = null;
  2616. //FOnce.mtf_advancedComments = null;
  2617. //FOnce.mtf_checkDescriptionLoaded = null;
  2618. //FOnce.mtf_forceCheckLiveVideo = null;
  2619. Q.mtf_chatBlockQ = null;
  2620.  
  2621.  
  2622. mtoInterval = mtoInterval1;
  2623.  
  2624. }
  2625.  
  2626. let comments_section_loaded_elm = null;
  2627.  
  2628.  
  2629.  
  2630. function getTabsHTML() {
  2631.  
  2632. const sTabBtnVideos = `${svgElm(16,16,298,298,svgVideos)}<span>Videos</span>`
  2633. const sTabBtnInfo = `${svgElm(16,16,23.625,23.625,svgInfo)}<span>Info</span>`
  2634. const sTabBtnPlayList = `${svgElm(16,16,426.667,426.667,svgPlayList)}<span>Playlist</span>`
  2635.  
  2636. let str1 = `
  2637. <paper-ripple class="style-scope yt-icon-button">
  2638. <div id="background" class="style-scope paper-ripple" style="opacity:0;"></div>
  2639. <div id="waves" class="style-scope paper-ripple"></div>
  2640. </paper-ripple>
  2641. `;
  2642.  
  2643. const str_tabs = [
  2644. `<a id="tab-btn1" data-name="info" userscript-tab-content="#tab-info" class="tab-btn">${sTabBtnInfo}${str1}</a>`,
  2645. `<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>`,
  2646. `<a id="tab-btn4" userscript-tab-content="#tab-videos" data-name="videos" class="tab-btn">${sTabBtnVideos}${str1}</a>`,
  2647. `<a id="tab-btn5" userscript-tab-content="#tab-list" class="tab-btn">${sTabBtnPlayList}${str1}</a>`
  2648. ].join('')
  2649.  
  2650. let addHTML = `
  2651. <div id="right-tabs">
  2652. <header>
  2653. <div id="material-tabs">
  2654. ${str_tabs}
  2655. </div>
  2656. </header>
  2657. <div class="tab-content">
  2658. <div id="tab-info" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2659. <div id="tab-comments" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2660. <div id="tab-videos" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2661. <div id="tab-list" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2662. </div>
  2663. </div>
  2664. `;
  2665.  
  2666. return addHTML
  2667.  
  2668. }
  2669.  
  2670.  
  2671. function onVideoChange() {
  2672. // for popstate & next video
  2673.  
  2674. newVideoPageCheck();
  2675.  
  2676. scheduledCommentRefresh=true;
  2677. let comments = document.querySelector('ytd-comments#comments')
  2678. if(!deferredVarYTDHidden && scheduledCommentRefresh && Q.comments_section_loaded>0 && comments.hasAttribute('hidden')){
  2679.  
  2680. scheduledCommentRefresh=false;
  2681. // comment tab btn - hide by timer in attribute change
  2682. resetCommentSection();
  2683. }
  2684.  
  2685. mtf_forceCheckLiveVideo_disable = 0
  2686. mtc_store= Date.now()+3870
  2687.  
  2688. if (deferredVarYTDHidden) { // reset info when hidden
  2689. resetCommentSection();
  2690. }else{
  2691. checkVisibleEngagementPanel();
  2692. atChange(); // in case no mutation occurance
  2693. }
  2694.  
  2695.  
  2696.  
  2697. }
  2698.  
  2699. function onNavigationEnd(evt) {
  2700. forceConfig();
  2701. /*
  2702. console.log(window.ytcfg)
  2703. try{
  2704. window.ytcfg.set({
  2705. "EXPERIMENT_FLAGS": {"kevlar_watch_metadata_refresh":false}})
  2706. }catch(e){}
  2707. */
  2708.  
  2709. newVideoPageCheck(); // required for init
  2710.  
  2711. let mRet =
  2712. (/^https?\:\/\/(\w+\.)*youtube\.com\/watch\?(\w+\=[^\/\?\&]+\&|\w+\=?\&)*v=[\w\-\_]+/.test(window.location.href) ? 1 : 0) +
  2713. (document.querySelector('ytd-watch-flexy[tabview-selection]') ? 2 : 0);
  2714.  
  2715. if (mRet === 1) {
  2716.  
  2717. document.addEventListener('loadedmetadata', function(evt) {
  2718. if (evt.target.matches('video[tabview-mainvideo]')) {
  2719. setTimeout(onVideoChange, 30)
  2720. }
  2721. }, true)
  2722.  
  2723. pluginUnhook(); // in case not triggered by popstate - say mini playing
  2724.  
  2725. let timeout = 4; // max. 4 animation frames
  2726.  
  2727. let tf = () => {
  2728.  
  2729. if (--timeout > 0 && !document.querySelector('#player video')) return requestAnimationFrame(tf);
  2730.  
  2731. if (!document.querySelector('script#userscript-tabview-injection-1')) {
  2732.  
  2733. if (isMyScriptInChromeRuntime()){
  2734.  
  2735. addScriptByURL(window.chrome.runtime.getURL('js/injection_script_1.js')).id = 'userscript-tabview-injection-1';
  2736. }
  2737. else{
  2738.  
  2739. let injection_script = GM_getResourceText("injectionJS1")
  2740. addScript(`${injection_script}`).id = 'userscript-tabview-injection-1';
  2741. }
  2742. }
  2743.  
  2744. let ytdFlexyElm = document.querySelector('ytd-watch-flexy')
  2745.  
  2746. if(!ytdFlexyElm) return;
  2747.  
  2748.  
  2749. scriptEnable = true;
  2750. scriptEC++;
  2751.  
  2752. no_fix_contents_until = 0;
  2753. no_fix_playlist_until = 0;
  2754.  
  2755. ytdFlexy = mWeakRef(ytdFlexyElm)
  2756.  
  2757. let timeoutR_findRelated = new Timeout();
  2758. timeoutR_findRelated.set(function() {
  2759. let ytdFlexyElm = kRef(ytdFlexy);
  2760. if(!ytdFlexyElm) return true;
  2761. let related = ytdFlexyElm.querySelector("#related");
  2762. if (!related) return true;
  2763. foundRelated(related);
  2764. }, 100, 10)
  2765.  
  2766. function foundRelated(related) {
  2767. let promise = Promise.resolve();
  2768. if (!document.querySelector("#right-tabs")) {
  2769. promise = promise.then(() => {
  2770. $(getTabsHTML()).insertBefore(related).attr('data-dom-created-by-tabview-youtube', scriptVersionForExternal);
  2771. })
  2772. }
  2773. promise.then(runAfterTabAppended)
  2774. }
  2775.  
  2776. /*
  2777. setTimeout(() => {
  2778. for (const s of document.querySelectorAll('#right-tabs [userscript-scrollbar-render]')) {
  2779. Promise.resolve(s).then(s => {
  2780. if (s && s.scrollTop > 0) s.scrollTop = 0;
  2781. let child = s.firstElementChild;
  2782. if (child && child.scrollTop > 0) child.scrollTop = 0;
  2783. });
  2784. }
  2785. }, 90)
  2786. */
  2787.  
  2788.  
  2789. }
  2790.  
  2791. tf();
  2792.  
  2793. } else if (mRet === 3) {
  2794.  
  2795. if(!scriptEnable) return;
  2796.  
  2797. let elmComments = document.querySelector('ytd-comments#comments')
  2798.  
  2799. if (elmComments) {
  2800. nativeFunc(elmComments, "loadComments")
  2801. }
  2802.  
  2803. } else if (mRet === 0) {
  2804.  
  2805. pluginUnhook(); // in case not triggered by popstate - say mini playing
  2806.  
  2807. }
  2808.  
  2809. if(mRet & 1){
  2810. setTimeout(()=>{
  2811. Q.mutationTarget=null;
  2812. atChange(); // in case no mutation occurance
  2813. FP.mtf_attrComments()
  2814. },160);
  2815. }
  2816.  
  2817. }
  2818.  
  2819.  
  2820. function setToActiveTab(defaultTab) {
  2821. if (isTheater() && isWideScreenWithTwoColumns()) return;
  2822. const jElm = document.querySelector(`a[userscript-tab-content="${switchTabActivity_lastTab}"]:not(.tab-btn-hidden)`) ||
  2823. document.querySelector(`a[userscript-tab-content="${(defaultTab||settings.defaultTab)}"]:not(.tab-btn-hidden)`) ||
  2824. document.querySelector("a[userscript-tab-content]:not(.tab-btn-hidden)") ||
  2825. null;
  2826. switchTabActivity(jElm);
  2827. return !!jElm;
  2828. }
  2829.  
  2830. function getWrapper(wrapperId) {
  2831. let $wrapper = $(`#${wrapperId}`);
  2832. if (!$wrapper[0]) $wrapper = $(`<div id="${wrapperId}"></div>`)
  2833. return $wrapper.first();
  2834. }
  2835.  
  2836. function runAfterTabAppended() {
  2837.  
  2838. document.documentElement.setAttribute('plugin-tabview-youtube', '')
  2839.  
  2840. const ytdFlexyElm = kRef(ytdFlexy)
  2841. if(!ytdFlexyElm) return;
  2842. if (!ytdFlexyElm.hasAttribute('tabview-selection')) ytdFlexyElm.setAttribute('tabview-selection', '')
  2843.  
  2844.  
  2845. //console.log('unc02')
  2846.  
  2847. // append the next videos
  2848. // it exists as "related" is already here
  2849. fixTabs();
  2850.  
  2851. // just switch to the default tab
  2852. setToActiveTab();
  2853.  
  2854.  
  2855. prepareTabBtn();
  2856.  
  2857.  
  2858. // append the detailed meta contents to the tab-info
  2859. // ** FOnce.mtf_checkDescriptionLoaded = FP.mtf_checkDescriptionLoaded;
  2860. // ** if (Q.mutationTarget === null) $callOnceAsync('mtf_checkDescriptionLoaded');
  2861.  
  2862. // force window scroll when #continuations is first detected and #comments still [hidden]
  2863. //FOnce.mtf_advancedComments = FP.mtf_advancedComments;
  2864. //if (Q.mutationTarget === null) $callOnceAsync('mtf_advancedComments');
  2865.  
  2866. // use video player's element to detect the live-chat situation (no commenting section)
  2867. // 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
  2868. //FOnce.mtf_forceCheckLiveVideo = FP.mtf_forceCheckLiveVideo;
  2869. //if (Q.mutationTarget === null) $callOnceAsync('mtf_forceCheckLiveVideo');
  2870. //mtf_forceCheckLiveVideo();
  2871.  
  2872.  
  2873. // Attr Mutation Observer - #playlist - hidden
  2874. //clearMutationObserver(mtoVs, 'mtoVisibility_Playlist')
  2875. // Attr Mutation Observer callback - #playlist - hidden
  2876.  
  2877. // pending for #playlist and set Attribute Observer
  2878. // ** FOnce.mtf_initalAttr_playlist = FP.mtf_initalAttr_playlist
  2879. // ** if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_playlist');
  2880.  
  2881. // Attr Mutation Observer - ytd-comments#comments - hidden
  2882. //clearMutationObserver(mtoVs, 'mtoVisibility_Comments')
  2883. // Attr Mutation Observer callback - ytd-comments#comments - hidden
  2884.  
  2885. // pending for #comments and set Attribute Observer
  2886. // ** FOnce.mtf_initalAttr_comments = FP.mtf_initalAttr_comments;
  2887. // ** if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_comments');
  2888.  
  2889.  
  2890. //clearMutationObserver(mtoVs, 'mtoVisibility_Chatroom');
  2891. //FOnce.mtf_initalAttr_chatroom = FP.mtf_initalAttr_chatroom
  2892. //if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_chatroom');
  2893.  
  2894. // clearMutationObserver(mtoVs, 'mtoVisibility_EngagementPanel');
  2895. // for (const engagement_panel of document.querySelectorAll('ytd-watch-flexy ytd-engagement-panel-section-list-renderer:not([tabview-attr-checked])')) {
  2896. // engagement_panel.removeAttribute('tabview-attr-checked');
  2897. // }
  2898. //FOnce.mtf_initalAttr_engagement_panel = FP.mtf_initalAttr_engagement_panel
  2899. //if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_engagement_panel');
  2900.  
  2901.  
  2902. mtoFlexyAttr.clear(true)
  2903. mtf_checkFlexy()
  2904.  
  2905.  
  2906. document.querySelector("#right-tabs .tab-content").addEventListener('scroll', makeBodyScrollByEvt, true);
  2907.  
  2908.  
  2909. Q.addP = 0;
  2910. Q.removeP = 0;
  2911. Q.mutationTarget = null;
  2912.  
  2913. Q.mtoNav_requestNo = 0;
  2914.  
  2915.  
  2916. mtoMutation_watchFlexy.bindElement(ytdFlexyElm, {
  2917. childList: true,
  2918. subtree: true,
  2919. attributes: false
  2920. })
  2921.  
  2922. // for automcomplete plugin or other userscript plugins
  2923. // document.body for Firefox >= 60
  2924. mtoMutation_body.bindElement(document.querySelector('body'), {
  2925. childList: true,
  2926. subtree: false,
  2927. attributes: false
  2928. })
  2929.  
  2930.  
  2931. regularCheck(1, 0, null);
  2932.  
  2933.  
  2934.  
  2935.  
  2936. }
  2937.  
  2938.  
  2939. function fetchCommentsFinished() {
  2940. let ytdFlexyElm = kRef(ytdFlexy);
  2941. if (!scriptEnable || !ytdFlexyElm) return;
  2942. if(mtf_forceCheckLiveVideo_disable===2) return;
  2943. akAttr(ytdFlexyElm, 'tabview-youtube-comments', false, 'LS')
  2944. }
  2945.  
  2946. function setCommentSection( /** @type {number} */ value) {
  2947.  
  2948. Q.comments_section_loaded = value;
  2949.  
  2950. if (value === 0) {
  2951. sect_lastUpdate = 0;
  2952. comments_section_loaded_elm = null;
  2953. }
  2954.  
  2955. }
  2956.  
  2957. function resetCommentSection(){
  2958.  
  2959. let tab_btn = document.querySelector('.tab-btn[userscript-tab-content="#tab-comments"]')
  2960.  
  2961.  
  2962. if(tab_btn){
  2963.  
  2964. let span = tab_btn.querySelector('span#tab3-txt-loader');
  2965.  
  2966. tab_btn.removeAttribute('loaded-comment')
  2967. tab_btn.classList.remove('tab-btn-hidden')
  2968. if(span){
  2969. span.textContent='';
  2970. }
  2971. }
  2972.  
  2973.  
  2974. setCommentSection(0);
  2975.  
  2976.  
  2977. }
  2978.  
  2979. function _disableComments() {
  2980. //requestingComments = null;
  2981. mtc_cid && timeline.clearInterval(mtc_cid)
  2982. mtc_cid=0;
  2983.  
  2984. if (!scriptEnable) return;
  2985. let cssElm = kRef(ytdFlexy);
  2986. if (!cssElm) return;
  2987.  
  2988. mtoInterval = mtoInterval2;
  2989.  
  2990. if(Q.comments_section_loaded>0) return; //already displayed / disabled
  2991.  
  2992. scheduledCommentRefresh=false;
  2993. setCommentSection(2);
  2994.  
  2995. comments_section_loaded_elm = mWeakRef(document.querySelector('ytd-comments#comments div#contents') || null)
  2996.  
  2997. let tabBtn = document.querySelector('.tab-btn[userscript-tab-content="#tab-comments"]');
  2998. if(tabBtn) {
  2999. tabBtn.removeAttribute('loaded-comment')
  3000. if (!tabBtn.classList.contains('tab-btn-hidden')) {
  3001. hideTabBtn(tabBtn)
  3002. }
  3003. }
  3004.  
  3005. akAttr(cssElm, 'tabview-youtube-comments', true, 'D');
  3006.  
  3007.  
  3008. }
  3009.  
  3010.  
  3011.  
  3012.  
  3013. let layoutStatusMutex = new Mutex();
  3014.  
  3015. function forceDisplayChatReplay() {
  3016. let items = chatFrameElement('yt-live-chat-item-list-renderer #items');
  3017. if (items && items.childElementCount !== 0) return;
  3018.  
  3019. let ytd_player = document.querySelector('ytd-player#ytd-player');
  3020. if (!ytd_player) return;
  3021. let videoElm = ytd_player.querySelector('video');
  3022. if (!videoElm) return;
  3023.  
  3024. let video = videoElm;
  3025. if (videoElm && video.currentTime > 0 && !video.ended && video.readyState > video.HAVE_CURRENT_DATA) {
  3026. let chat = document.querySelector('ytd-live-chat-frame#chat');
  3027. if (chat) {
  3028. nativeFunc(chat, "postToContentWindow", [{ "yt-player-video-progress": videoElm.currentTime }])
  3029. }
  3030. }
  3031.  
  3032. }
  3033.  
  3034.  
  3035.  
  3036. function runAfterExpandChat() {
  3037.  
  3038.  
  3039. if(runAfterExpandChat.cid_chatFrameCheck1) timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck1);
  3040. if(runAfterExpandChat.cid_chatFrameCheck2) timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck2);
  3041. runAfterExpandChat.cid_chatFrameCheck1=0;
  3042. runAfterExpandChat.cid_chatFrameCheck2=0;
  3043.  
  3044. new Promise(resolve => {
  3045.  
  3046. let chatFrame_st = Date.now();
  3047. runAfterExpandChat.cid_chatFrameCheck1 = 0;
  3048.  
  3049. let sEF = new ScriptEF();
  3050. runAfterExpandChat.cid_chatFrameCheck1 = timeline.setInterval(() => {
  3051. if (!sEF.isValid()) return runAfterExpandChat.cid_chatFrameCheck1 = timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck1);
  3052. let cDoc = chatFrameContentDocument();
  3053. if (cDoc) {
  3054. runAfterExpandChat.cid_chatFrameCheck1 = timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck1);
  3055. resolve();
  3056. } else if (!scriptEnable || !isChatExpand() || Date.now() - chatFrame_st > 6750) {
  3057. runAfterExpandChat.cid_chatFrameCheck1 = timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck1);
  3058. }
  3059. }, 60);
  3060.  
  3061.  
  3062. }).then(() => new Promise(resolve => {
  3063.  
  3064.  
  3065. let chatFrame_st = Date.now();
  3066. runAfterExpandChat.cid_chatFrameCheck2 = 0;
  3067.  
  3068. let sEF = new ScriptEF();
  3069. runAfterExpandChat.cid_chatFrameCheck2 = timeline.setInterval(() => {
  3070. if (!sEF.isValid()) return runAfterExpandChat.cid_chatFrameCheck2 = timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck2);
  3071. let app = chatFrameElement('yt-live-chat-app');
  3072. if (app) {
  3073. runAfterExpandChat.cid_chatFrameCheck2 = timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck2);
  3074. resolve(app);
  3075. } else if (!scriptEnable || !isChatExpand() || Date.now() - chatFrame_st > 6750*3) {
  3076. runAfterExpandChat.cid_chatFrameCheck2 = timeline.clearInterval(runAfterExpandChat.cid_chatFrameCheck2);
  3077. }
  3078. }, 60);
  3079.  
  3080.  
  3081. })).then(app => {
  3082.  
  3083.  
  3084. let cDoc = app.ownerDocument;
  3085.  
  3086. if (!scriptEnable || !isChatExpand()) return;
  3087. if(cDoc.querySelector('#userscript-tabview-chatroom-css')) return;
  3088. addStyle(`
  3089. body #input-panel.yt-live-chat-renderer::after {
  3090. background: transparent;
  3091. }
  3092. #items.yt-live-chat-item-list-renderer{
  3093. contain: content;
  3094. }
  3095. yt-live-chat-text-message-renderer{
  3096. contain: content;
  3097. }
  3098. #item-offset.yt-live-chat-item-list-renderer{
  3099. contain: content;
  3100. }
  3101. #item-scroller.yt-live-chat-item-list-renderer{
  3102. contain: strict;
  3103. }
  3104. img[width][height]{
  3105. contain: strict;
  3106. }
  3107. #item-list > yt-live-chat-item-list-renderer, #item-list > yt-live-chat-item-list-renderer > #contents{
  3108. contain: strict;
  3109. }
  3110.  
  3111.  
  3112. #chat.style-scope.yt-live-chat-renderer,
  3113. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer,
  3114. #item-scroller,
  3115. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer .style-scope.yt-live-chat-text-message-renderer,
  3116. yt-live-chat-ticker-paid-message-item-renderer
  3117. {
  3118. contain: layout paint style;
  3119. }
  3120. yt-img-shadow#author-photo.style-scope{
  3121. contain: layout paint style;
  3122. content-visibility: auto;
  3123. contain-intrinsic-size: 24px 24px;
  3124. }
  3125. #item-offset.style-scope.yt-live-chat-item-list-renderer,
  3126. #items.style-scope.yt-live-chat-item-list-renderer {
  3127. contain: layout paint;
  3128. }
  3129. .style-scope.yt-live-chat-text-message-renderer {
  3130. cursor: default;
  3131. }
  3132. #author-photo.style-scope.yt-live-chat-text-message-renderer,
  3133. yt-live-chat-author-chip.style-scope.yt-live-chat-text-message-renderer,
  3134. yt-live-chat-author-chip.style-scope.yt-live-chat-text-message-renderer ~ span#message.style-scope.yt-live-chat-text-message-renderer
  3135. {
  3136. pointer-events: none;
  3137. }
  3138. span#message.style-scope.yt-live-chat-text-message-renderer > img.emoji.yt-formatted-string.style-scope.yt-live-chat-text-message-renderer{
  3139. contain: layout paint style;
  3140. cursor: default;
  3141. pointer-events: none;
  3142. }
  3143. body yt-live-chat-app{
  3144. contain: size layout paint style;
  3145. content-visibility: auto;
  3146. transform: translate3d(0,0,0);
  3147. overflow: hidden;
  3148. }
  3149.  
  3150.  
  3151. /*
  3152. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer:nth-last-of-type(n+20){
  3153. content-visibility: auto;
  3154. }
  3155. */
  3156.  
  3157. /*
  3158. @supports (contain-intrinsic-size: auto 1px){
  3159.  
  3160. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer{
  3161. content-visibility: auto;
  3162. contain-intrinsic-size: auto 24px;
  3163. }
  3164.  
  3165. }
  3166. */
  3167. .style-scope.yt-live-chat-item-list-renderer{
  3168. box-sizing: border-box;
  3169. }
  3170.  
  3171. /*
  3172. .style-scope.yt-live-chat-item-list-renderer[style*="--tabview-crm-height"]{
  3173. content-visibility: auto;
  3174. contain-intrinsic-size: 1px var(--tabview-crm-height, 24px);
  3175. }
  3176.  
  3177. #item-offset.style-scope.yt-live-chat-item-list-renderer{
  3178. height: auto !important;
  3179. }
  3180. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items:only-child{
  3181. position: relative !important;
  3182. }*/
  3183.  
  3184. #item-offset.style-scope.yt-live-chat-item-list-renderer{
  3185. contain: size layout paint style;
  3186. }
  3187. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items:only-child{
  3188. contain: layout paint style;
  3189. }
  3190.  
  3191. `, cDoc.documentElement).id='userscript-tabview-chatroom-css'
  3192.  
  3193. let frc= 0;
  3194. let fullReady = ()=>{
  3195.  
  3196.  
  3197. if(!cDoc.documentElement.hasAttribute('style') && ++frc<900) return timeline.setTimeout(fullReady,10);
  3198.  
  3199. if (cDoc.querySelector('yt-live-chat-renderer #continuations')) {
  3200. timeline.setTimeout(() => mtf_ChatExist(), 40);
  3201. $(document.querySelector('ytd-live-chat-frame#chat')).attr('yt-userscript-iframe-loaded', '')
  3202. }
  3203.  
  3204. forceDisplayChatReplay();
  3205.  
  3206. let iframe = document.querySelector('ytd-live-chat-frame iframe#chatframe');
  3207.  
  3208.  
  3209. iframe.dispatchEvent(new CustomEvent("tabview-chatroom-ready"))
  3210.  
  3211.  
  3212.  
  3213. }
  3214. fullReady();
  3215.  
  3216.  
  3217.  
  3218. })
  3219.  
  3220. }
  3221.  
  3222. function flexyAttr_toggleFlag(mFlag, b, flag) {
  3223. if (b) mFlag = mFlag | flag;
  3224. else mFlag = mFlag & ~flag;
  3225. return mFlag;
  3226. }
  3227.  
  3228. function flexAttr_toLayoutStatus(nls, attributeName) {
  3229.  
  3230. let attrElm, b, v;
  3231. switch (attributeName) {
  3232. case 'theater':
  3233. nls = flexyAttr_toggleFlag(nls, isTheater(), LAYOUT_THEATER);
  3234. break;
  3235. case 'userscript-chat-collapsed':
  3236. case 'userscript-chatblock':
  3237. attrElm = kRef(ytdFlexy);
  3238. if (hasAttribute(attrElm, 'userscript-chat-collapsed')) {
  3239. nls = flexyAttr_toggleFlag(nls, true, LAYOUT_CHATROOM | LAYOUT_CHATROOM_COLLASPED);
  3240. } else {
  3241. nls = flexyAttr_toggleFlag(nls, hasAttribute(attrElm, 'userscript-chatblock'), LAYOUT_CHATROOM);
  3242. nls = flexyAttr_toggleFlag(nls, false, LAYOUT_CHATROOM_COLLASPED);
  3243. }
  3244. break;
  3245. case 'is-two-columns_':
  3246. nls = flexyAttr_toggleFlag(nls, isWideScreenWithTwoColumns(), LAYOUT_TWO_COLUMNS);
  3247. break;
  3248.  
  3249. case 'tabview-selection':
  3250. b = isNonEmptyString(kRef(ytdFlexy).getAttribute('tabview-selection'));
  3251. nls = flexyAttr_toggleFlag(nls, b, LAYOUT_TAB_EXPANDED);
  3252. break;
  3253.  
  3254. case 'fullscreen':
  3255. b = isNonEmptyString(kRef(ytdFlexy).getAttribute('fullscreen'));
  3256. nls = flexyAttr_toggleFlag(nls, b, LAYOUT_FULLSCREEN);
  3257. break;
  3258.  
  3259. case 'userscript-engagement-panel':
  3260. v = kRef(ytdFlexy).getAttribute('userscript-engagement-panel');
  3261. b = (+v > 0)
  3262. nls = flexyAttr_toggleFlag(nls, b, LAYOUT_ENGAGEMENT_PANEL_EXPAND);
  3263. break;
  3264.  
  3265. }
  3266.  
  3267. return nls;
  3268.  
  3269.  
  3270. }
  3271.  
  3272. let mtf_attrFlexy = (mutations, observer) => {
  3273.  
  3274. //attr mutation checker - $$ytdFlexyElm$$ {ytd-watch-flexy} \single
  3275. //::attr
  3276. // ~ 'userscript-chat-collapsed', 'userscript-chatblock', 'theater', 'is-two-columns_',
  3277. // ~ 'tabview-selection', 'fullscreen', 'userscript-engagement-panel',
  3278. // ~ 'hidden'
  3279.  
  3280. //console.log(15330, scriptEnable, kRef(ytdFlexy), mutations)
  3281.  
  3282. if (!scriptEnable) return;
  3283.  
  3284. const cssElm = kRef(ytdFlexy)
  3285. if (!cssElm) return;
  3286.  
  3287. if (!mutations) return;
  3288.  
  3289. //console.log(15340)
  3290. ytdFlexyHiddenCheck(mutations);
  3291.  
  3292. const old_layoutStatus = wls.layoutStatus
  3293. if (old_layoutStatus === null) return;
  3294. let new_layoutStatus = old_layoutStatus;
  3295.  
  3296. let checkedChat = false;
  3297.  
  3298. for (const mutation of mutations) {
  3299. new_layoutStatus = flexAttr_toLayoutStatus(new_layoutStatus, mutation.attributeName);
  3300. if (mutation.attributeName == 'userscript-chat-collapsed' || mutation.attributeName == 'userscript-chatblock') {
  3301.  
  3302. if(!checkedChat){
  3303. checkedChat = true; // avoid double call
  3304.  
  3305. if (cssElm.getAttribute('userscript-chatblock') === 'chat-live') {
  3306. // assigned new attribute - "chat-live" => disable comments section
  3307. _disableComments();
  3308. }
  3309.  
  3310. if (!cssElm.hasAttribute('userscript-chatblock')) {
  3311. // might or might not collapsed before
  3312. timeline.setTimeout(() => {
  3313. if (!scriptEnable) return;
  3314. //delayed call => check with the "no active focus" condition with chatroom status
  3315. if (!isAnyActiveTab() && !isChatExpand() && !isTheater() && isWideScreenWithTwoColumns() && !isFullScreen()) {
  3316. setToActiveTab();
  3317. }
  3318. }, 240);
  3319. }
  3320. }
  3321.  
  3322. }else if(mutation.attributeName == 'userscript-engagement-panel'){
  3323. // assume any other active component such as tab content and chatroom
  3324. if (+(cssElm.getAttribute('userscript-engagement-panel')||0)===0 && +mutation.oldValue>0) {
  3325. timeline.setTimeout(() => {
  3326. if (!scriptEnable) return;
  3327. //delayed call => check with the "no active focus" condition with engagement panel status
  3328. if (!isAnyActiveTab() && !isEngagementPanelExpanded() && !isTheater() && isWideScreenWithTwoColumns() && !isFullScreen() && !isChatExpand()) {
  3329. setToActiveTab();
  3330. }
  3331. }, 240);
  3332. }
  3333. }
  3334. }
  3335.  
  3336. if (new_layoutStatus !== old_layoutStatus) wls.layoutStatus = new_layoutStatus
  3337.  
  3338. }
  3339.  
  3340. const mtf_checkFlexy = () => {
  3341. // once per $$native-ytd-watch-flexy$$ {ytd-watch-flexy} detection
  3342.  
  3343. let ytdFlexyElm = kRef(ytdFlexy);
  3344. if (!scriptEnable || !ytdFlexyElm) return true;
  3345.  
  3346.  
  3347. wls.layoutStatus = null;
  3348.  
  3349. let isFlexyHidden = (deferredVarYTDHidden = ytdFlexyElm.hasAttribute('hidden'));
  3350.  
  3351. if (!isFlexyHidden) {
  3352. let rChatExist = base_ChatExist();
  3353. if (rChatExist) {
  3354. let { attr_chatblock, attr_chatcollapsed } = rChatExist;
  3355. if (attr_chatblock === null) {
  3356. //remove attribute if it is unknown
  3357. attr_chatblock = false;
  3358. attr_chatcollapsed = false;
  3359. }
  3360. wAttr(ytdFlexyElm, 'userscript-chatblock', attr_chatblock)
  3361. wAttr(ytdFlexyElm, 'userscript-chat-collapsed', attr_chatcollapsed)
  3362. }
  3363. }
  3364.  
  3365. let rTabSelection = [...ytdFlexyElm.querySelectorAll('.tab-btn[userscript-tab-content]')]
  3366. .map(elm => ({ elm, hidden: elm.classList.contains('tab-btn-hidden') }));
  3367.  
  3368. if(rTabSelection.length === 0){
  3369. wAttr(ytdFlexyElm, 'tabview-selection', false);
  3370. }else{
  3371. rTabSelection=rTabSelection.filter(entry => entry.hidden !== true); // all available tabs
  3372. if (rTabSelection.length === 0) wAttr(ytdFlexyElm, 'tabview-selection', '');
  3373. }
  3374. rTabSelection = null;
  3375. let rEP = engagement_panels_();
  3376. if (rEP && rEP.count > 0) wAttr(ytdFlexyElm, 'userscript-engagement-panel', false);
  3377. //else wAttr(ytdFlexyElm, 'userscript-engagement-panel', rEP.value + ""); // can be 0
  3378. else if(rEP.value>0) wAttr(ytdFlexyElm, 'userscript-engagement-panel', rEP.value + ""); // can be 0
  3379.  
  3380. let ls = 0;
  3381. ls = flexAttr_toLayoutStatus(ls, 'theater')
  3382. ls = flexAttr_toLayoutStatus(ls, 'userscript-chat-collapsed')
  3383. ls = flexAttr_toLayoutStatus(ls, 'userscript-chatblock')
  3384. ls = flexAttr_toLayoutStatus(ls, 'is-two-columns_')
  3385. ls = flexAttr_toLayoutStatus(ls, 'tabview-selection')
  3386. ls = flexAttr_toLayoutStatus(ls, 'fullscreen')
  3387. ls = flexAttr_toLayoutStatus(ls, 'userscript-engagement-panel')
  3388.  
  3389. wls.layoutStatus = ls
  3390.  
  3391. mtoFlexyAttr.bindElement(ytdFlexyElm,{
  3392. attributes: true,
  3393. attributeFilter: ['userscript-chat-collapsed', 'userscript-chatblock', 'theater', 'is-two-columns_', 'tabview-selection', 'fullscreen', 'userscript-engagement-panel', 'hidden'],
  3394. attributeOldValue: true
  3395. })
  3396.  
  3397. ytdFlexyHiddenCheckBasic(ytdFlexyElm);
  3398.  
  3399.  
  3400. let columns = document.querySelector('ytd-page-manager#page-manager #columns')
  3401. if (columns) {
  3402. wAttr(columns, 'userscript-scrollbar-render', true);
  3403. }
  3404.  
  3405. return false;
  3406. }
  3407.  
  3408. function ytdFlexyHiddenCheckBasic(ytdFlexyElm){
  3409. // for both hidden and non-hidden ytd-watch-flexy
  3410.  
  3411. deferredVarYTDHidden = ytdFlexyElm.hasAttribute('hidden');
  3412.  
  3413. // console.log(15403)
  3414. document.documentElement.classList.toggle('tabview-normal-player',!deferredVarYTDHidden)
  3415.  
  3416. }
  3417.  
  3418. function checkVisibleEngagementPanel(){
  3419. if(storeLastPanel){
  3420.  
  3421. let elm_storeLastPanel = kRef(storeLastPanel);
  3422.  
  3423. if(elm_storeLastPanel && !isDOMVisible(elm_storeLastPanel) ){
  3424. storeLastPanel=null;
  3425. ytBtnCloseEngagementPanels();
  3426. }
  3427.  
  3428. }
  3429.  
  3430. }
  3431.  
  3432. function ytdFlexyHiddenCheck(mutations) {
  3433.  
  3434. //console.log(15350)
  3435.  
  3436. const ytdFlexyElm = kRef(ytdFlexy)
  3437. if (!ytdFlexyElm) return;
  3438. let muHidden = false
  3439.  
  3440. for (const mutation of mutations) {
  3441. if (mutation.attributeName === 'hidden') {
  3442. //console.log(343)
  3443. muHidden = true;
  3444. break;
  3445. }
  3446. }
  3447.  
  3448. if (!muHidden) return;
  3449.  
  3450. mtc_store= Date.now()+2870
  3451. //console.log(7004)
  3452. deferredVarYTDHidden = ytdFlexyElm.hasAttribute('hidden');
  3453.  
  3454. if(deferredVarYTDHidden && scheduledCommentRefresh && Q.comments_section_loaded>0){
  3455. scheduledCommentRefresh=false;
  3456. }
  3457.  
  3458. //console.log(15400)
  3459. if(!deferredVarYTDHidden){
  3460. mtf_forceCheckLiveVideo_disable = 0
  3461. timeline.setTimeout(function() {
  3462. mtf_ChatExist();
  3463. layoutStatusMutex.lockWith(unlock=>{
  3464. atChange(); // in case no mutation occurance
  3465. ytdFlexyHiddenCheckBasic(ytdFlexyElm);
  3466. dispatchWindowResize(); // player control positioning
  3467. timeline.setTimeout(unlock, 40);
  3468. })
  3469. }, 40);
  3470. }else{
  3471. //console.log(15401)
  3472. timeline.setTimeout(function() {
  3473. //console.log(15402)
  3474. ytdFlexyHiddenCheckBasic(ytdFlexyElm);
  3475. }, 40);
  3476. }
  3477.  
  3478. }
  3479.  
  3480.  
  3481.  
  3482.  
  3483. let switchTabActivity_lastTab = null
  3484.  
  3485. function setDisplayedPlaylist() {
  3486. //override the default youtube coding event prevention
  3487. let cssElm = kRef(ytdFlexy);
  3488. if (!scriptEnable || !cssElm) return;
  3489. displayedPlaylist = mWeakRef(document.querySelector('ytd-watch-flexy #tab-list:not(.tab-content-hidden) ytd-playlist-panel-renderer') || null);
  3490. }
  3491.  
  3492.  
  3493. let recordScrollTop = 0
  3494.  
  3495. function switchTabActivity(activeLink) {
  3496. if (!scriptEnable) return;
  3497.  
  3498. const ytdFlexyElm = kRef(ytdFlexy);
  3499.  
  3500. if (!ytdFlexyElm) return;
  3501.  
  3502. if (activeLink && activeLink.classList.contains('tab-btn-hidden')) return; // not allow to switch to hide tab
  3503.  
  3504. if (isTheater() && isWideScreenWithTwoColumns()) activeLink = null;
  3505.  
  3506. /*
  3507. let isPrevTabComments = document.querySelector('.tab-content-cld:not(.tab-content-hidden)');
  3508. isPrevTabComments=isPrevTabComments&&isPrevTabComments.matches('#tab-comments')
  3509.  
  3510. if(isPrevTabComments){
  3511. let comments_tab = document.querySelector('#tab-comments');
  3512. if(comments_tab){
  3513. let st = comments_tab.scrollTop;
  3514. if(st>=0) recordScrollTop = st;
  3515. }
  3516.  
  3517.  
  3518. }
  3519. */
  3520.  
  3521. function runAtEnd() {
  3522.  
  3523. //console.log(12312)
  3524.  
  3525. if (activeLink) lastShowTab = activeLink.getAttribute('userscript-tab-content')
  3526.  
  3527. displayedPlaylist = null;
  3528. scrollingVideosList = null;
  3529.  
  3530. if (activeLink && lastShowTab == '#tab-list') {
  3531. setDisplayedPlaylist();
  3532. } else if (activeLink && lastShowTab == '#tab-videos') {
  3533. scrollingVideosList = mWeakRef(document.querySelector('ytd-watch-flexy #tab-videos:not(.tab-content-hidden) [placeholder-videos]'));
  3534. }
  3535.  
  3536.  
  3537. ytdFlexyElm.setAttribute('tabview-selection', activeLink ? lastShowTab : '')
  3538.  
  3539. if (lastShowTab == '#tab-comments' && (ytdFlexyElm.getAttribute('tabview-youtube-comments') || '').lastIndexOf('S') >= 0) {
  3540.  
  3541. if(mtf_forceCheckLiveVideo_disable===2) {}
  3542. else{
  3543. akAttr(ytdFlexyElm, 'tabview-youtube-comments', false, 'L');
  3544.  
  3545. /*
  3546. requestAnimationFrame(() => {
  3547. let comments_tab = document.querySelector('#tab-comments');
  3548. if (comments_tab && comments_tab.scrollTop > 0) comments_tab.scrollTop = 0;
  3549. });
  3550. */
  3551. }
  3552.  
  3553. }
  3554.  
  3555. /*
  3556. let isNewTabComments = document.querySelector('.tab-content-cld:not(.tab-content-hidden)')
  3557. isNewTabComments=isNewTabComments&&isNewTabComments.matches('#tab-comments')
  3558.  
  3559. if(isNewTabComments){
  3560. let comments_tab = document.querySelector('#tab-comments');
  3561. if(comments_tab){
  3562. let st = comments_tab.scrollTop;
  3563. if(Math.abs(st-recordScrollTop)>0.9) comments_tab.scrollTop=recordScrollTop;
  3564. }
  3565.  
  3566. }
  3567. */
  3568.  
  3569.  
  3570. }
  3571.  
  3572. const links = document.querySelectorAll('#material-tabs a[userscript-tab-content]');
  3573.  
  3574.  
  3575.  
  3576. for (const link of links) {
  3577. let content = document.querySelector(link.getAttribute('userscript-tab-content'));
  3578. if (link && content) {
  3579. if (link !== activeLink) {
  3580. link.classList.remove("active");
  3581. content.classList.add("tab-content-hidden");
  3582. } else {
  3583. link.classList.add("active");
  3584. content.classList.remove("tab-content-hidden");
  3585. //timeline.setTimeout(()=>content.focus(),400);
  3586.  
  3587. }
  3588. }
  3589. }
  3590.  
  3591. runAtEnd();
  3592.  
  3593.  
  3594. }
  3595.  
  3596. let tabsUiScript_setclick = false;
  3597.  
  3598. function prepareTabBtn() {
  3599.  
  3600. const materialTab = document.querySelector("#material-tabs")
  3601. if (!materialTab) return;
  3602.  
  3603. let noActiveTab = !!document.querySelector('ytd-watch-flexy[userscript-chatblock]:not([userscript-chat-collapsed])')
  3604.  
  3605. const activeLink = materialTab.querySelector('a[userscript-tab-content].active:not(.tab-btn-hidden)')
  3606. if (activeLink) switchTabActivity(noActiveTab ? null : activeLink)
  3607.  
  3608. if (!tabsUiScript_setclick) {
  3609. tabsUiScript_setclick = true;
  3610. $(materialTab).on("click", "a", function(evt) {
  3611.  
  3612. //console.log(8510)
  3613. let ytdFlexyElm = kRef(ytdFlexy);
  3614. if (!scriptEnable || !ytdFlexyElm) return null;
  3615.  
  3616. if (!this.hasAttribute('userscript-tab-content')) return;
  3617.  
  3618.  
  3619. evt.preventDefault();
  3620.  
  3621. //console.log(8511)
  3622.  
  3623. /*
  3624. if (this.getAttribute('userscript-tab-content') == '#tab-comments' && parseInt(ytdFlexyElm.getAttribute('tabview-youtube-comments') || '') < 0) {
  3625. console.log(8512)
  3626. return;
  3627. }*/
  3628.  
  3629. //console.log(8513)
  3630. new Promise(requestAnimationFrame).then(() => {
  3631.  
  3632.  
  3633. //console.log(8514)
  3634. layoutStatusMutex.lockWith(unlock => {
  3635.  
  3636. //console.log(8515)
  3637. switchTabActivity_lastTab = this.getAttribute('userscript-tab-content');
  3638.  
  3639. let isActiveAndVisible = this.classList.contains('tab-btn') && this.classList.contains('active') && !this.classList.contains('tab-btn-hidden')
  3640.  
  3641. if (isWideScreenWithTwoColumns() && !isTheater() && isActiveAndVisible) {
  3642. //optional
  3643. timeline.setTimeout(unlock, 80);
  3644. switchTabActivity(null);
  3645. ytBtnSetTheater();
  3646. } else if (isActiveAndVisible) {
  3647. timeline.setTimeout(unlock, 80);
  3648. switchTabActivity(null);
  3649. } else {
  3650. let pInterval = 60;
  3651. if (isChatExpand() && isWideScreenWithTwoColumns()) {
  3652. ytBtnCollapseChat();
  3653. } else if (isEngagementPanelExpanded() && isWideScreenWithTwoColumns()) {
  3654. ytBtnCloseEngagementPanels();
  3655. } else if (isWideScreenWithTwoColumns() && isTheater() && !isFullScreen()) {
  3656. ytBtnCancelTheater();
  3657. } else {
  3658. pInterval = 20;
  3659. }
  3660. //console.log(8518)
  3661. timeline.setTimeout(() => {
  3662. timeline.setTimeout(makeBodyScroll, 20); // this is to make the image render
  3663.  
  3664. timeline.setTimeout(() => {
  3665. let rightTabs = document.querySelector('#right-tabs');
  3666. if (!isWideScreenWithTwoColumns() && rightTabs && rightTabs.offsetTop > 0 && this.classList.contains('active')) {
  3667. let tabButtonBar = document.querySelector('#material-tabs');
  3668. let tabButtonBarHeight = tabButtonBar ? tabButtonBar.offsetHeight : 0;
  3669. window.scrollTo(0, rightTabs.offsetTop - tabButtonBarHeight);
  3670. }
  3671. }, 60)
  3672. // console.log(8519)
  3673.  
  3674. timeline.setTimeout(unlock, 80)
  3675. switchTabActivity(this)
  3676.  
  3677. }, pInterval);
  3678. }
  3679.  
  3680.  
  3681. })
  3682.  
  3683. })
  3684.  
  3685.  
  3686.  
  3687.  
  3688. });
  3689.  
  3690. }
  3691.  
  3692. }
  3693.  
  3694.  
  3695. // ---------------------------------------------------------------------------------------------
  3696. document.addEventListener("yt-navigate-finish", onNavigationEnd)
  3697. //yt-navigate-redirect
  3698. //"yt-page-data-fetched"
  3699. //yt-navigate-error
  3700. //yt-navigate-start
  3701. //yt-page-manager-navigate-start
  3702. //"yt-navigate"
  3703. //"yt-navigate-cache
  3704.  
  3705. document.addEventListener("yt-navigate-cache",()=>{
  3706. console.log('yt-navigate-cache')
  3707. })
  3708. document.addEventListener("yt-navigate-redirect",()=>{
  3709. console.log('yt-navigate-redirect')
  3710. })
  3711.  
  3712. function forceConfig(){
  3713. let trial = false;
  3714. let f=()=>{
  3715. if(trial) return;
  3716. let b = true;
  3717. try{
  3718. document.dispatchEvent(new CustomEvent("me-func"))
  3719. window.yt.config_.EXPERIMENT_FLAGS.kevlar_watch_metadata_refresh=true
  3720. }catch(e){
  3721. b=false;
  3722. }
  3723. if(b) trial = true;
  3724.  
  3725. }
  3726. f();
  3727. requestAnimationFrame(f)
  3728. setTimeout(f,30)
  3729. setTimeout(f,300)
  3730. }
  3731.  
  3732. document.addEventListener("yt-navigate-start",()=>{
  3733. console.log('yt-navigate-start')
  3734. forceConfig();
  3735. })
  3736.  
  3737. document.addEventListener("yt-page-manager-navigate-start",()=>{
  3738. console.log('yt-page-manager-navigate-start')
  3739. forceConfig();
  3740. })
  3741.  
  3742.  
  3743. document.addEventListener("loadstart", (evt) => {
  3744. if (!evt || !evt.target || evt.target.nodeName !== "VIDEO") return;
  3745. let elm = evt.target;
  3746. if (!elm.matches('#player video, #movie_player video, video[tabview-mainvideo]')) return;
  3747.  
  3748. mtc_store= Date.now()+2870
  3749.  
  3750. let src = elm.src;
  3751. if (src !== lastVideoURL) {
  3752. lastVideoURL = elm.src;
  3753. elm.setAttribute('tabview-mainvideo', ''); // mainly for mini playing
  3754. }
  3755.  
  3756. }, true)
  3757.  
  3758. // ---------------------------------------------------------------------------------------------
  3759.  
  3760. let scrolling_lastD = 0;
  3761.  
  3762. const singleColumnScrolling = function(/** @type {boolean} */ scrolling_lastF) {
  3763. if (!scriptEnable || deferredVarYTDHidden) return;
  3764.  
  3765. let pageY = scrollY;
  3766. if (pageY < 10 && scrolling_lastD === 0 && !scrolling_lastF) return;
  3767.  
  3768. let targetElm, header, navElm;
  3769.  
  3770. Promise.resolve().then(() => {
  3771.  
  3772. targetElm = document.querySelector("#right-tabs");
  3773. if (!targetElm) return;
  3774. header = targetElm.querySelector("header");
  3775. if (!header) return;
  3776. navElm = document.querySelector('#masthead-container, #masthead')
  3777. if (!navElm) return;
  3778. let navHeight = navElm ? navElm.offsetHeight : 0
  3779.  
  3780. let elmY = targetElm.offsetTop
  3781.  
  3782. let xyz = [elmY + navHeight, pageY, elmY - navHeight]
  3783.  
  3784. let xyStatus = 0
  3785. if (xyz[1] < xyz[2] && xyz[2] < xyz[0]) {
  3786. // 1
  3787. xyStatus = 1
  3788. }
  3789.  
  3790. if (xyz[0] > xyz[1] && xyz[1] > xyz[2]) {
  3791.  
  3792. //2
  3793. xyStatus = 2
  3794.  
  3795. }
  3796.  
  3797. if (xyz[2] < xyz[0] && xyz[0] < xyz[1]) {
  3798. // 3
  3799.  
  3800. xyStatus = 3
  3801.  
  3802.  
  3803. }
  3804.  
  3805. return xyStatus;
  3806.  
  3807. }).then((xyStatus) => {
  3808.  
  3809. if ((xyStatus == 2 || xyStatus == 3) && (scrolling_lastD === 0 || scrolling_lastF)) {
  3810. scrolling_lastD = 1;
  3811. let {
  3812. offsetHeight
  3813. } = header
  3814. let {
  3815. offsetWidth
  3816. } = targetElm
  3817.  
  3818. targetElm.style.setProperty("--userscript-sticky-width", offsetWidth + 'px')
  3819. targetElm.style.setProperty("--userscript-sticky", offsetHeight + 'px')
  3820.  
  3821. wAttr(targetElm, 'userscript-sticky', true);
  3822.  
  3823. } else if ((xyStatus == 1) && (scrolling_lastD === 1 || scrolling_lastF)) {
  3824. scrolling_lastD = 0;
  3825.  
  3826. wAttr(targetElm, 'userscript-sticky', false);
  3827. }
  3828.  
  3829.  
  3830. targetElm = null;
  3831. header = null;
  3832. navElm = null;
  3833.  
  3834. });
  3835.  
  3836. };
  3837.  
  3838. window.addEventListener("scroll", function() {
  3839. singleColumnScrolling(false)
  3840. }, {
  3841. capture: false,
  3842. passive: true
  3843. })
  3844.  
  3845. let lastResizeAt = 0;
  3846. window.addEventListener('resize', function() {
  3847.  
  3848. if (!scriptEnable) return;
  3849. if (deferredVarYTDHidden) return;
  3850. lastResizeAt = Date.now();
  3851.  
  3852. requestAnimationFrame(() => {
  3853. singleColumnScrolling(true)
  3854. })
  3855.  
  3856. }, {
  3857. capture: false,
  3858. passive: true
  3859. })
  3860.  
  3861.  
  3862.  
  3863. let hist = null
  3864. const getHistState = (history)=>`${history.length}|${history.state?history.state.entryTime:null}`;
  3865. function newVideoPageCheck(){
  3866. let hState = getHistState(history)
  3867. if(hist!==hState) {
  3868. hist=hState;
  3869. if(scriptEnable){
  3870. //scriptEnable = false;
  3871. let callTimeout = !newVideoPage.entryTime
  3872. newVideoPage.entryTime = 1
  3873. if(callTimeout) setTimeout(newVideoPage,0)
  3874. }
  3875. return true;
  3876. }
  3877. }
  3878. function newVideoPage(){
  3879. newVideoPage.entryTime = 0;
  3880.  
  3881. console.log('newVideoPage')
  3882. //console.log('newVideoPage-', 150, location.href)
  3883.  
  3884. let ytdFlexyElm = kRef(ytdFlexy);
  3885. if(!ytdFlexyElm) return;
  3886.  
  3887. timeline.reset();
  3888. layoutStatusMutex = new Mutex();
  3889.  
  3890. //console.log('newVideoPage-', 350, location.href)
  3891. let flag1= /^https?\:\/\/(\w+\.)*youtube\.com\/watch\?(\w+\=[^\/\?\&]+\&|\w+\=?\&)*v=[\w\-\_]+/.test(location.href)
  3892. let tf = ()=>{
  3893.  
  3894. let isFlexyHidden = (deferredVarYTDHidden = ytdFlexyElm.hasAttribute('hidden'));
  3895.  
  3896. let flag2 = !isFlexyHidden;
  3897.  
  3898. if(flag1^flag2) return timeline.setTimeout(tf,7); else funcVisibleWatch();
  3899.  
  3900. tf= null;
  3901. funcVisibleWatch = null;
  3902. ytdFlexyElm=null
  3903.  
  3904. }
  3905.  
  3906. let funcVisibleWatch = ()=>{
  3907. //scriptEnable = true;
  3908.  
  3909. let isFlexyHidden = (deferredVarYTDHidden = ytdFlexyElm.hasAttribute('hidden'));
  3910. ytdFlexyHiddenCheckBasic(ytdFlexyElm)
  3911.  
  3912. if(!isFlexyHidden) {
  3913.  
  3914. mtf_forceCheckLiveVideo_disable = 0;
  3915. mtc_store= Date.now()+2870
  3916.  
  3917. sect_lastUpdate = 0;
  3918. if(pendingOne){
  3919. // reset pendingOne = false if the element is already removed.
  3920. pendingOne = document.querySelectorAll(`ytd-watch-flexy ytd-comments#comments [tabview-cache-time]`).length>1;
  3921. }
  3922.  
  3923. timeline.setTimeout(()=>{
  3924. Q.mutationTarget=null;
  3925. FP.mtf_attrComments()
  3926. },2870)
  3927. if(pageInit_attrComments) hookSection();
  3928.  
  3929.  
  3930.  
  3931.  
  3932.  
  3933. let new_layoutStatus = wls.layoutStatus
  3934.  
  3935. const new_isExpandedChat = !(new_layoutStatus & LAYOUT_CHATROOM_COLLASPED) && (new_layoutStatus & LAYOUT_CHATROOM)
  3936. const new_isCollaspedChat = (new_layoutStatus & LAYOUT_CHATROOM_COLLASPED) && (new_layoutStatus & LAYOUT_CHATROOM)
  3937. const new_isTwoColumns = new_layoutStatus & LAYOUT_TWO_COLUMNS;
  3938. const new_isTheater = new_layoutStatus & LAYOUT_THEATER;
  3939. const new_isTabExpanded = new_layoutStatus & LAYOUT_TAB_EXPANDED;
  3940. const new_isFullScreen = new_layoutStatus & LAYOUT_FULLSCREEN;
  3941. const new_isExpandEPanel = new_layoutStatus & LAYOUT_ENGAGEMENT_PANEL_EXPAND;
  3942. if( ytdFlexyElm.getAttribute('tabview-selection')==='' && new_isTwoColumns && !new_isTheater && !new_isTabExpanded && !new_isFullScreen && !new_isExpandEPanel && !new_isExpandedChat ){
  3943. // e.g. engage panel removed after miniview and change video
  3944. setToActiveTab();
  3945. }else if(new_isExpandEPanel && ytdFlexyElm.querySelectorAll('ytd-engagement-panel-section-list-renderer[visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"]').length===0){
  3946. wls.layoutStatus = new_layoutStatus & (~LAYOUT_ENGAGEMENT_PANEL_EXPAND)
  3947. }
  3948.  
  3949. //mtf_attrFlexy();
  3950.  
  3951. FP.mtf_attrEngagementPanel();
  3952. FP.mtf_attrPlaylist();
  3953. FP.mtf_attrComments();
  3954. FP.mtf_attrChatroom();
  3955.  
  3956.  
  3957. }
  3958.  
  3959. regularCheck(1,0,null); // mutation happens when the page is not ready; call again as the page is ready.
  3960.  
  3961.  
  3962.  
  3963.  
  3964. }
  3965.  
  3966. tf();
  3967.  
  3968.  
  3969. }
  3970.  
  3971.  
  3972.  
  3973. function browserHistoryState(){
  3974. let maxCount = 5
  3975.  
  3976. if (!scriptEnable) return;
  3977. let ytdFlexyElm = kRef(ytdFlexy);
  3978. if(!ytdFlexyElm) return;
  3979.  
  3980. let cid = timeline.setInterval(function(){
  3981. if(newVideoPageCheck()){
  3982. timeline.clearInterval(cid);
  3983. }
  3984. if(--maxCount===0) timeline.clearInterval(cid);
  3985. },7)
  3986.  
  3987. }
  3988.  
  3989.  
  3990. window.addEventListener('hashchange', function() {
  3991. if (!scriptEnable) return;
  3992. console.log('hashchange');
  3993. browserHistoryState();
  3994. }, { capture: true })
  3995.  
  3996. window.addEventListener('popstate', function() {
  3997. if (!scriptEnable) return;
  3998. console.log('popstate');
  3999. browserHistoryState();
  4000. }, { capture: true })
  4001.  
  4002.  
  4003. // function clearMutationObserver(o, key) {
  4004. // if (o[key]) {
  4005. // o[key].takeRecords();
  4006. // o[key].disconnect();
  4007. // o[key] = null;
  4008. // return true;
  4009. // }
  4010. // }
  4011.  
  4012. // function initMutationObserver(o, key, callback) {
  4013. // clearMutationObserver(o, key);
  4014. // const mto = new MutationObserver(callback);
  4015. // o[key] = mto;
  4016. // return mto;
  4017. // }
  4018. // function initMutationObserver2(o, key, mtoFunc) {
  4019. // clearMutationObserver(o, key);
  4020. // const mto = mtoFunc();
  4021. // o[key] = mto;
  4022. // return mto;
  4023. // }
  4024.  
  4025. document.addEventListener('wheel', function(evt) {
  4026.  
  4027. if (!scriptEnable) return;
  4028. const displayedPlaylist_element = kRef(displayedPlaylist);
  4029. if (displayedPlaylist_element && displayedPlaylist_element.contains(evt.target)) {
  4030. evt.stopPropagation();
  4031. evt.stopImmediatePropagation();
  4032. }
  4033. }, { capture: true, passive: true });
  4034.  
  4035.  
  4036. function setVideosTwoColumns(/** @type {number} */ flag, /** @type {boolean} */ bool) {
  4037.  
  4038. //two columns to one column
  4039.  
  4040. /*
  4041. [placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy
  4042.  
  4043. is-two-columns ="" => no is-two-columns
  4044. [placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer
  4045. no hidden => hidden =""
  4046. [placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer
  4047. hidden ="" => no hidden
  4048.  
  4049. */
  4050.  
  4051. let cssSelector1 = '[placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy'
  4052.  
  4053. let cssSelector2 = '[placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer'
  4054.  
  4055. let cssSelector3 = '[placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer'
  4056.  
  4057. let res = {}
  4058. if (flag & 1) {
  4059. res.m1 = document.querySelector(cssSelector1)
  4060. if (res.m1) wAttr(res.m1, 'is-two-columns', bool ? '' : false);
  4061. }
  4062.  
  4063. if (flag & 2) {
  4064. res.m2 = document.querySelector(cssSelector2)
  4065. if (res.m2) wAttr(res.m2, 'hidden', bool ? false : '');
  4066. }
  4067.  
  4068. if (flag & 4) {
  4069. res.m3 = document.querySelector(cssSelector3)
  4070. if (res.m3) wAttr(res.m3, 'hidden', bool ? '' : false);
  4071. }
  4072.  
  4073.  
  4074. return res
  4075.  
  4076.  
  4077.  
  4078.  
  4079. }
  4080.  
  4081. let lastScrollFetch = 0;
  4082. // function isScrolledToEnd(){
  4083. // return (window.innerHeight + window.pageYOffset) >= document.scrollingElement.scrollHeight - 2;
  4084. // }
  4085. let lastOffsetTop = 0;
  4086. window.addEventListener('scroll', function(evt) {
  4087.  
  4088. //console.log(evt.target)
  4089.  
  4090. if (!scriptEnable) return;
  4091.  
  4092.  
  4093. if (!kRef(scrollingVideosList)) return;
  4094. if (videoListBeforeSearch) return;
  4095.  
  4096.  
  4097.  
  4098. let visibleHeight = document.scrollingElement.clientHeight;
  4099. let totalHeight = document.scrollingElement.scrollHeight;
  4100.  
  4101. if (totalHeight < visibleHeight * 1.5) return; // filter out two column view;
  4102.  
  4103. let z = window.pageYOffset + visibleHeight;
  4104. let h_advanced = totalHeight - (visibleHeight > 5 * 40 ? visibleHeight * 0.5 : 40);
  4105.  
  4106.  
  4107.  
  4108. if (z > h_advanced && !isWideScreenWithTwoColumns()) {
  4109.  
  4110. let ct = Date.now();
  4111. if (ct - lastScrollFetch < 500) return; //prevent continuous calling
  4112.  
  4113. lastScrollFetch = ct;
  4114.  
  4115. let res = setVideosTwoColumns(2 | 4, true)
  4116. if (res.m3 && res.m2) {
  4117.  
  4118. //wait for DOM change, just in case
  4119. requestAnimationFrame(() => {
  4120. let { offsetTop } = res.m2 // as visibility of m2 & m3 switched.
  4121.  
  4122. if (offsetTop - lastOffsetTop < 40) return; // in case bug, or repeating calling. // the next button shall below the this button
  4123. lastOffsetTop = offsetTop
  4124.  
  4125. res.m2.parentNode.dispatchEvent(new Event('yt-service-request-sent-button-renderer'))
  4126.  
  4127. res = null
  4128. })
  4129.  
  4130. } else {
  4131.  
  4132. res = null
  4133. }
  4134.  
  4135.  
  4136. }
  4137.  
  4138.  
  4139.  
  4140.  
  4141. }, { passive: true })
  4142.  
  4143. let sect_lastUpdate = 0;
  4144.  
  4145. let sect_holder = null;
  4146. let sect_hText = null;
  4147. let sect_hTime = 0;
  4148.  
  4149. let pendingOne = false;
  4150. let mtf_forceCheckLiveVideo_disable = 0;
  4151.  
  4152. let storeLastPanel = null;
  4153.  
  4154. /** @type {boolean | null} */
  4155. let deferredVarYTDHidden = null;
  4156. let scheduledCommentRefresh = false;
  4157.  
  4158. let pageInit_attrComments = false;
  4159.  
  4160.  
  4161.  
  4162. let mgChatFrame = {
  4163. setVar(elm) {
  4164. mgChatFrame.kVar = mWeakRef(elm)
  4165. },
  4166. getVar() {
  4167. return kRef(mgChatFrame.kVar)
  4168. },
  4169. inPage() {
  4170. let elm = mgChatFrame.getVar();
  4171. if (!elm) return false;
  4172. let ytdFlexyElm = kRef(ytdFlexy);
  4173. if(!ytdFlexyElm) return false;
  4174. return ytdFlexyElm.contains(elm)
  4175. }
  4176. };
  4177.  
  4178. const timeline = {
  4179. // after initialized (initObserver)
  4180. cn1:{},
  4181. cn2:{},
  4182. setTimeout( /** @type {TimerHandler} */ f,/** @type {number} */ d){
  4183. return timeline.cn1[setTimeout(f,d)]=true
  4184. },
  4185. clearTimeout(/** @type {number} */ cid){
  4186. timeline.cn1[cid]=false; return clearTimeout(cid)
  4187. },
  4188. setInterval(/** @type {TimerHandler} */ f,/** @type {number} */ d){
  4189. return timeline.cn2[setInterval(f,d)]=true
  4190. },
  4191. clearInterval(/** @type {number} */ cid){
  4192. timeline.cn2[cid]=false; return clearInterval(cid)
  4193. },
  4194. reset(){
  4195. for(let cid in timeline.cn1) timeline.cn1[cid] && clearTimeout(cid)
  4196. for(let cid in timeline.cn2) timeline.cn2[cid] && clearInterval(cid)
  4197. timeline.cn1={}
  4198. timeline.cn2={}
  4199. }
  4200. }
  4201.  
  4202. class AttributeMutationObserver extends MutationObserver {
  4203. constructor(flist){
  4204. super((mutations, observer)=>{
  4205. for(const mutation of mutations){
  4206. if (mutation.type === 'attributes') {
  4207. this.checker(mutation.target, mutation.attributeName)
  4208. }
  4209. }
  4210. })
  4211. this.flist=flist;
  4212. this.res={}
  4213. }
  4214. takeRecords(){
  4215. super.takeRecords();
  4216. }
  4217. disconnect(){
  4218. this._target = null;
  4219. super.disconnect();
  4220. }
  4221. observe(/** @type {Node} */ target){
  4222. if(this._target) return;
  4223. this._target = mWeakRef(target);
  4224. const options = {
  4225. attributes: true,
  4226. attributeFilter: Object.keys(this.flist),
  4227. //attributeFilter: [ "status", "username" ],
  4228. attributeOldValue: true
  4229. }
  4230. super.observe(target, options)
  4231. }
  4232. checker(/** @type {Node} */ target,/** @type {string} */ attributeName){
  4233. let nv = target.getAttribute(attributeName);
  4234. if(this.res[attributeName]!==nv){
  4235. this.res[attributeName] = nv
  4236. let f = this.flist[attributeName];
  4237. if(f) f(attributeName, nv);
  4238.  
  4239. }
  4240. }
  4241. check(delay = 0){
  4242. setTimeout(()=>{
  4243. let target = kRef(this._target)
  4244. for(const key of Object.keys(this.flist)){
  4245. this.checker(target,key)
  4246. }
  4247. target = null;
  4248. },delay)
  4249. }
  4250. }
  4251.  
  4252. function goYoutubeGeniusLyrics(){
  4253.  
  4254. setTimeout(function $f(){
  4255.  
  4256. if(!document.documentElement.hasAttribute('w-engagement-panel-genius-lyrics')) return setTimeout($f,100)
  4257.  
  4258. document.documentElement.dispatchEvent(new CustomEvent('engagement-panel-genius-lyrics'))
  4259.  
  4260.  
  4261. },100)
  4262.  
  4263.  
  4264.  
  4265. }
  4266.  
  4267.  
  4268. /*
  4269.  
  4270. To be implement: timeline
  4271.  
  4272. timeline.setTimeout
  4273. timeline.clearTimeout
  4274. timeline.setInterval
  4275. timeline.clearInterval
  4276.  
  4277. */
  4278.  
  4279.  
  4280.  
  4281. //Object.keys($0).filter(key=>!(key in $0.constructor.prototype))
  4282.  
  4283. //Object.getOwnPropertyNames(window).filter(k=>k.startsWith('HTML'))
  4284. //Object.getOwnPropertyNames(window).filter(k=>k.startsWith('HTML')).filter(k=>$0 instanceof window[k])
  4285.  
  4286.  
  4287. /* --------------------------- browser's bug in -webkit-box ----------------------------------------- */
  4288.  
  4289. /*
  4290. fix bug for comment section - version 1.8.7
  4291. This issue is the bug in browser's rendering
  4292. I guess, this is due to the lines clamp with display:-webkit-box
  4293. use stupid coding to let it re-render when its content become visible
  4294. /*
  4295.  
  4296. ytd-expander[should-use-number-of-lines][collapsed] > #content.ytd-expander {
  4297. color: var(--yt-spec-text-primary);
  4298. display: -webkit-box;
  4299. overflow: hidden;
  4300. max-height: none;
  4301. -webkit-box-orient: vertical;
  4302. -webkit-line-clamp: var(--ytd-expander-max-lines, 4);
  4303. }
  4304.  
  4305. // v1.8.36 imposed a effective solution for fixing this bug
  4306.  
  4307. */
  4308.  
  4309. /* --------------------------- browser's bug in -webkit-box ----------------------------------------- */
  4310.  
  4311.  
  4312. /**
  4313. *
  4314.  
  4315.  
  4316. f.initChildrenObserver=function(){var a=this;this.observer=new MutationObserver(function(){return a.childrenChanged()});
  4317. this.observer.observe(this.$.content,{subtree:!0,childList:!0,attributes:!0});this.childrenChanged()};
  4318. f.childrenChanged=function(){var a=this;this.alwaysToggleable?this.canToggle=this.alwaysToggleable:this.canToggleJobId||(this.canToggleJobId=window.requestAnimationFrame(function(){$h(function(){a.canToggleJobId=0;a.calculateCanCollapse()})}))};
  4319.  
  4320.  
  4321. f.onIronResize=function(){this.recomputeOnResize&&this.childrenChanged()};
  4322.  
  4323.  
  4324. onButtonClick_:function(){this.fire("yt-close-upsell-dialog")},
  4325. computeHasHeader_:function(a){return!!a.headerBackgroundImage}});var geb;var heb;var ieb;var jeb;var xI=function(){var a=L.apply(this,arguments)||this;a.alignAuto=!1;a.collapsed=!0;a.isToggled=!1;a.alwaysCollapsed=!1;a.canToggle=!0;a.collapsedHeight=80;a.disableToggle=!1;a.alwaysToggleable=!1;a.reversed=!1;a.shouldUseNumberOfLines=!1;a.recomputeOnResize=!1;a.canToggleJobId=0;return a};
  4326. n(xI,L);f=xI.prototype;f.alwaysToggleableChanged=function(){this.alwaysToggleable&&(this.canToggle=!0)};
  4327.  
  4328.  
  4329. f.calculateCanCollapse=function(){this.canToggle=this.shouldUseNumberOfLines?this.alwaysToggleable||this.$.content.offsetHeight<this.$.content.scrollHeight:this.alwaysToggleable||this.$.content.scrollHeight>this.collapsedHeight};
  4330. f.detachObserver=function(){this.observer&&this.observer.disconnect()};
  4331.  
  4332. *
  4333. *
  4334. *
  4335. */
  4336.  
  4337.  
  4338.  
  4339.  
  4340. })();
  4341.  
  4342.  
  4343.  
  4344.  
  4345.  
  4346.  
  4347.  
  4348.  
  4349.  
  4350.  
  4351.  
  4352.  
  4353.  
  4354.  
  4355.  
  4356.  
  4357.  
  4358.  
  4359.  
  4360.  
  4361.  
  4362.  
  4363.  
  4364.  
  4365.  
  4366.  
  4367.  
  4368.  
  4369.  
  4370.  
  4371.  
  4372.  
  4373.  
  4374.  
  4375.  
  4376.  
  4377.  
  4378.  
  4379.  
  4380.  
  4381.  
  4382.  
  4383. // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js
  4384.  
  4385. }
  4386.  
  4387.  
  4388. ;!(function $$() {
  4389. 'use strict';
  4390.  
  4391. if(document.documentElement==null) return window.requestAnimationFrame($$)
  4392.  
  4393. const cssTxt = GM_getResourceText("contentCSS");
  4394.  
  4395. function addStyle (styleText) {
  4396. const styleNode = document.createElement('style');
  4397. styleNode.type = 'text/css';
  4398. styleNode.textContent = styleText;
  4399. document.documentElement.appendChild(styleNode);
  4400. return styleNode;
  4401. }
  4402.  
  4403. addStyle (cssTxt);
  4404.  
  4405. main(window.$);
  4406.  
  4407.  
  4408. // Your code here...
  4409. })();