Tabview Youtube

Make comments and lists into tabs

当前为 2022-04-29 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Tabview Youtube
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.8.42
  5. // @description Make comments and lists into tabs
  6. // @author CY Fung
  7. // @match https://www.youtube.com/*
  8. // @resource contentCSS https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube/a7d332f398a4ba137e32a3e90469a9ff047aa7d1/css/style_content.css
  9. // @icon https://github.com/cyfung1031/Tabview-Youtube/raw/main/images/icon128p.png
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/cash/8.1.0/cash.min.js
  11. // @grant GM_getResourceText
  12. // @run-at document-start
  13. // @license MIT https://github.com/cyfung1031/Tabview-Youtube/blob/main/LICENSE
  14. // @noframes
  15. // ==/UserScript==
  16.  
  17. /* jshint esversion:6 */
  18.  
  19. function main($){
  20. // MIT License
  21. // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js
  22.  
  23.  
  24.  
  25.  
  26.  
  27.  
  28.  
  29.  
  30.  
  31.  
  32.  
  33.  
  34.  
  35.  
  36.  
  37.  
  38.  
  39.  
  40.  
  41.  
  42.  
  43.  
  44.  
  45.  
  46. -(function() {
  47.  
  48. function inIframe() {
  49. try {
  50. return window.self !== window.top;
  51. } catch (e) {
  52. return true;
  53. }
  54. }
  55.  
  56. if (inIframe()) return;
  57.  
  58. if (!$) return;
  59.  
  60. /**
  61. * SVG resources:
  62. * <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>
  63. */
  64.  
  65. const scriptVersionForExternal = '2021/07/03';
  66.  
  67. const githubURLBase = "https://raw.githubusercontent.com/cyfung1031/Tabview-Youtube";
  68. const githubURLCommit = "bdf401045266e5224663f80b276bc7f56d122b8d";
  69.  
  70. const isMyScriptInChromeRuntime = () => {
  71. try{
  72. return typeof(window.chrome?.runtime?.getURL) == 'function'
  73. }catch(e){
  74. return false;
  75. }
  76. }
  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;
  135.  
  136.  
  137. const WeakRef = window.WeakRef;
  138. const mWeakRef = WeakRef ? (o => o ? new WeakRef(o) : null) : (o => o || null);
  139. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  140.  
  141. let cmItem = null;
  142.  
  143.  
  144. function tracer(key, cmp) {
  145. if (cmp > 0) return tracer[key] === cmp;
  146. return (tracer[key] = Date.now());
  147. }
  148.  
  149. function racer(key, f) {
  150. let now = Date.now();
  151. const kTime = `${key}$$1`
  152. let t = racer[kTime] || 0;
  153.  
  154. if (now < t) {
  155. const kCount = `${key}$$2`;
  156. racer[kCount] = (racer[kCount] || 0) + 1;
  157. if (racer[kCount] === 1) {
  158. let g = f;
  159. requestAnimationFrame(() => {
  160. racer[kCount] = 0;
  161. g();
  162. g = null;
  163. })
  164. }
  165. } else {
  166. racer[kTime] = now + 16;
  167. f();
  168. }
  169. }
  170.  
  171. class ScriptEF {
  172. constructor() {
  173. this._id = scriptEC;
  174. }
  175. isValid() {
  176. return this._id === scriptEC;
  177. }
  178. }
  179.  
  180. class Timeout {
  181.  
  182. set(f, d, repeatCount) {
  183. if (this.cid > 0) return;
  184. let sEF = new ScriptEF();
  185. if (repeatCount > 0) {
  186.  
  187. let rc = repeatCount;
  188. const g = () => {
  189. this.cid = 0;
  190. if (!sEF.isValid()) return;
  191. let res = f();
  192. if (--rc <= 0) return;
  193. if (res === true) this.cid = setTimeout(g, d);
  194. }
  195. g();
  196.  
  197. } else {
  198.  
  199. const g = () => {
  200. this.cid = 0;
  201. if (!sEF.isValid()) return;
  202. if (f() === true) this.cid = setTimeout(g, d);
  203. }
  204. this.cid = setTimeout(g, d);
  205. }
  206. }
  207.  
  208. clear() {
  209. if (this.cid > 0) clearTimeout(this.cid);
  210. }
  211.  
  212. isEmpty() {
  213. return !this.cid
  214. }
  215.  
  216.  
  217. }
  218.  
  219. class Mutex {
  220.  
  221. constructor() {
  222. this.p = Promise.resolve()
  223. }
  224.  
  225. lockWith(f) {
  226.  
  227. this.p = this.p.then(() => {
  228. return new Promise(f)
  229. }).catch(console.warn)
  230. }
  231.  
  232. }
  233.  
  234.  
  235.  
  236. function prettyElm(elm) {
  237. if (!elm || !elm.nodeName) return null;
  238. const eId = elm.id || null;
  239. const eClsName = elm.className || null;
  240. return [elm.nodeName.toLowerCase(), typeof eId == 'string' ? "#" + eId : '', typeof eClsName == 'string' ? '.' + eClsName.replace(/\s+/g, '.') : ''].join('').trim();
  241. }
  242.  
  243. function extractTextContent(elm) {
  244. 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, '')
  245. }
  246.  
  247. function addScript(scriptText) {
  248. const scriptNode = document.createElement('script');
  249. scriptNode.type = 'text/javascript';
  250. scriptNode.textContent = scriptText;
  251. document.documentElement.appendChild(scriptNode);
  252. return scriptNode;
  253. }
  254.  
  255. function addScriptByURL(scriptURL) {
  256. const scriptNode = document.createElement('script');
  257. scriptNode.type = 'text/javascript';
  258. scriptNode.src = scriptURL;
  259. document.documentElement.appendChild(scriptNode);
  260. return scriptNode;
  261. }
  262.  
  263. function addStyle(styleText, container) {
  264. const styleNode = document.createElement('style');
  265. //styleNode.type = 'text/css';
  266. styleNode.textContent = styleText;
  267. (container || document.documentElement).appendChild(styleNode);
  268. return styleNode;
  269. }
  270.  
  271.  
  272.  
  273.  
  274. function isDOMVisible(elem) {
  275. // jQuery version : https://github.com/jquery/jquery/blob/a684e6ba836f7c553968d7d026ed7941e1a612d8/src/css/hiddenVisibleSelectors.js
  276. return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
  277. }
  278.  
  279. function isNonEmptyString(s) {
  280. return typeof s == 'string' && s.length > 0;
  281. }
  282.  
  283.  
  284. function nativeFunc(dom, property, args) {
  285. dom.dispatchEvent(new CustomEvent("userscript-call-dom-func", { detail: { property, args } }))
  286. }
  287.  
  288. function akAttr(cssElm, attrName, isNegative, flag) {
  289.  
  290. let u = parseInt(cssElm.getAttribute(attrName) || 0) || 0;
  291. let ak = Math.abs(u);
  292.  
  293. if (ak > 100 && isNegative && u < 0) {
  294.  
  295. } else if (ak > 100 && !isNegative && u > 0) {
  296.  
  297. } else {
  298. if (ak <= 100) {
  299. ak = 101;
  300. } else {
  301. ak++;
  302. if (ak >= 800) ak = 101;
  303. }
  304. // 101, 102, ... 799, 101
  305. }
  306.  
  307. let s = '' + (isNegative ? -ak : ak);
  308. flag = flag || '';
  309.  
  310.  
  311. cssElm.setAttribute(attrName, s + flag)
  312. }
  313.  
  314.  
  315.  
  316. let timeout_resize_for_layout_change = new Timeout();
  317.  
  318.  
  319.  
  320. function layoutStatusChanged(old_layoutStatus, new_layoutStatus) {
  321.  
  322.  
  323. if (old_layoutStatus === new_layoutStatus) return;
  324.  
  325. const cssElm = kRef(ytdFlexy);
  326.  
  327. if (!cssElm) return;
  328.  
  329.  
  330. const new_isExpandedChat = !(new_layoutStatus & LAYOUT_CHATROOM_COLLASPED) && (new_layoutStatus & LAYOUT_CHATROOM)
  331. const new_isCollaspedChat = (new_layoutStatus & LAYOUT_CHATROOM_COLLASPED) && (new_layoutStatus & LAYOUT_CHATROOM)
  332.  
  333. const new_isTwoColumns = new_layoutStatus & LAYOUT_TWO_COLUMNS;
  334. const new_isTheater = new_layoutStatus & LAYOUT_THEATER;
  335. const new_isTabExpanded = new_layoutStatus & LAYOUT_TAB_EXPANDED;
  336. const new_isFullScreen = new_layoutStatus & LAYOUT_FULLSCREEN;
  337. const new_isExpandEPanel = new_layoutStatus & LAYOUT_ENGAGEMENT_PANEL_EXPAND;
  338.  
  339.  
  340. function showTabOrChat() {
  341.  
  342. layoutStatusMutex.lockWith(unlock => {
  343.  
  344. if (lastShowTab == '#chatroom') {
  345.  
  346. if (new_isTabExpanded) switchTabActivity(null)
  347. if (!new_isExpandedChat) ytBtnExpandChat();
  348.  
  349. } else if (lastShowTab && lastShowTab.indexOf('#engagement-panel-') == 0) {
  350.  
  351. if (new_isTabExpanded) switchTabActivity(null)
  352. if (!new_isExpandEPanel) ytBtnOpenEngagementPanel(lastShowTab);
  353.  
  354. } else {
  355.  
  356. if (new_isExpandedChat) ytBtnCollapseChat()
  357. if (!new_isTabExpanded) setToActiveTab();
  358.  
  359. }
  360.  
  361. setTimeout(unlock, 40);
  362.  
  363. })
  364. }
  365.  
  366. function hideTabAndChat() {
  367.  
  368. layoutStatusMutex.lockWith(unlock => {
  369.  
  370. if (new_isTabExpanded) switchTabActivity(null)
  371. if (new_isExpandedChat) ytBtnCollapseChat()
  372. if (new_isExpandEPanel) ytBtnCloseEngagementPanels();
  373.  
  374.  
  375. setTimeout(unlock, 40);
  376.  
  377. })
  378.  
  379. }
  380.  
  381.  
  382. if (new_isExpandedChat || new_isTabExpanded || new_isExpandEPanel) {
  383. if (statusCollasped !== 1) statusCollasped = 1;
  384. } else {
  385. if (statusCollasped === 1) statusCollasped = 2;
  386. }
  387.  
  388. let changes = 0;
  389.  
  390. if (old_layoutStatus !== null) changes = old_layoutStatus ^ new_layoutStatus;
  391.  
  392. let chat_collasped_changed = !!(changes & LAYOUT_CHATROOM_COLLASPED)
  393. let tab_expanded_changed = !!(changes & LAYOUT_TAB_EXPANDED)
  394. let theater_mode_changed = !!(changes & LAYOUT_THEATER)
  395. let column_mode_changed = !!(changes & LAYOUT_TWO_COLUMNS)
  396. let fullscreen_mode_changed = !!(changes & LAYOUT_FULLSCREEN)
  397. let epanel_expanded_changed = !!(changes & LAYOUT_ENGAGEMENT_PANEL_EXPAND)
  398.  
  399. let tab_change = (tab_expanded_changed ? 1 : 0) | (chat_collasped_changed ? 2 : 0) | (epanel_expanded_changed ? 4 : 0);
  400.  
  401. let isChatOrTabExpandTriggering = tab_change == 0 ? false : (
  402. (tab_expanded_changed && new_isTabExpanded) ||
  403. (chat_collasped_changed && new_isExpandedChat) ||
  404. (epanel_expanded_changed && new_isExpandEPanel)
  405. );
  406.  
  407. let isChatOrTabCollaspeTriggering = tab_change == 0 ? false : (
  408. (tab_expanded_changed && !new_isTabExpanded) ||
  409. (chat_collasped_changed && new_isCollaspedChat) ||
  410. (epanel_expanded_changed && !new_isExpandEPanel)
  411. );
  412.  
  413.  
  414.  
  415. let moreThanOneShown = (new_isTabExpanded + new_isExpandedChat + new_isExpandEPanel) > 1
  416.  
  417. let requestVideoResize = false;
  418.  
  419. if (fullscreen_mode_changed || new_isFullScreen) {
  420.  
  421. } else if (tab_change == 0 && column_mode_changed && new_isTwoColumns && !new_isTheater && statusCollasped === 1 && moreThanOneShown) {
  422.  
  423. showTabOrChat();
  424. requestVideoResize = true;
  425.  
  426. } else if (tab_change == 2 && new_isExpandedChat && new_isTwoColumns && !new_isTheater && statusCollasped === 1 && new_isTabExpanded && !column_mode_changed) {
  427.  
  428. switchTabActivity(null);
  429. requestVideoResize = true;
  430.  
  431. } else if (isChatOrTabExpandTriggering && new_isTwoColumns && new_isTheater && statusCollasped === 1 && !theater_mode_changed && !column_mode_changed) {
  432.  
  433. ytBtnCancelTheater();
  434. requestVideoResize = true;
  435.  
  436. } else if (new_isTwoColumns && new_isTheater && statusCollasped === 1) {
  437.  
  438. hideTabAndChat();
  439. requestVideoResize = true;
  440.  
  441. } else if (isChatOrTabCollaspeTriggering && new_isTwoColumns && !new_isTheater && statusCollasped === 2 && !column_mode_changed) {
  442.  
  443. ytBtnSetTheater();
  444. requestVideoResize = true;
  445.  
  446. } else if (tab_change == 0 && (column_mode_changed || theater_mode_changed) && new_isTwoColumns && !new_isTheater && statusCollasped !== 1) {
  447.  
  448. showTabOrChat();
  449. requestVideoResize = true;
  450.  
  451. } else if (!new_isFullScreen && new_isTwoColumns && !new_isTheater && !new_isTabExpanded &&
  452. (new_isCollaspedChat || !new_isExpandedChat) &&
  453. !new_isExpandEPanel
  454. ) {
  455. // bug fix for restoring from mini player
  456.  
  457. layoutStatusMutex.lockWith(unlock => {
  458.  
  459. if (new_isExpandedChat) ytBtnCollapseChat()
  460. setToActiveTab();
  461.  
  462. setTimeout(unlock, 40);
  463.  
  464. })
  465.  
  466. requestVideoResize = true;
  467.  
  468. } else if (tab_expanded_changed) {
  469.  
  470. requestVideoResize = true;
  471.  
  472. }
  473.  
  474.  
  475. if (column_mode_changed && !chat_collasped_changed && new_isExpandedChat) {
  476.  
  477. runAfterExpandChat();
  478.  
  479. }
  480.  
  481.  
  482.  
  483. if (requestVideoResize) {
  484.  
  485. timeout_resize_for_layout_change.clear();
  486. timeout_resize_for_layout_change.set(() => {
  487. window.dispatchEvent(new Event('resize'))
  488. }, 92)
  489.  
  490. } else if (timeout_resize_for_layout_change.isEmpty() && (Date.now()) - lastResizeAt > 600) {
  491. timeout_resize_for_layout_change.set(() => {
  492. if ((Date.now()) - lastResizeAt > 600) window.dispatchEvent(new Event('resize'));
  493. }, 62)
  494. }
  495.  
  496.  
  497.  
  498.  
  499. }
  500.  
  501.  
  502. const $ws = {
  503. _layoutStatus: null,
  504. layoutStatus_pending: false
  505. }
  506.  
  507. let wls = new class {
  508. get layoutStatus() {
  509. return this._layoutStatus;
  510. }
  511. set layoutStatus(nv) {
  512.  
  513. if (nv === null) {
  514. this._layoutStatus = null;
  515. statusCollasped = 0;
  516. return;
  517. }
  518. if (nv === this._layoutStatus) return;
  519.  
  520. if (!this.layoutStatus_pending) {
  521. this.layoutStatus_pending = true;
  522. const old_layoutStatus = this._layoutStatus;
  523.  
  524. layoutStatusMutex.lockWith(unlock => {
  525.  
  526. this.layoutStatus_pending = false;
  527. const new_layoutStatus = this._layoutStatus;
  528. layoutStatusChanged(old_layoutStatus, new_layoutStatus);
  529.  
  530. setTimeout(unlock, 40)
  531.  
  532.  
  533. })
  534. }
  535.  
  536. this._layoutStatus = nv;
  537. }
  538. };
  539.  
  540.  
  541.  
  542.  
  543. const svgElm = (w, h, vw, vh, p) => `<svg width="${w}" height="${h}" viewBox="0 0 ${vw} ${vh}" preserveAspectRatio="xMidYMid meet">${p}</svg>`
  544.  
  545. let settings = {
  546. defaultTab: "#tab-videos"
  547. };
  548.  
  549.  
  550. let mtoInterval = mtoInterval1;
  551.  
  552. function isVideoPlaying(video) {
  553. return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > video.HAVE_CURRENT_DATA;
  554. }
  555.  
  556. function wAttr(elm, attr, kv) {
  557. 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) }
  558. }
  559.  
  560. function hideTabBtn(tabBtn) {
  561. var isActiveBefore = tabBtn.classList.contains('active');
  562. tabBtn.classList.add("tab-btn-hidden");
  563. if (isActiveBefore) {
  564. setToActiveTab();
  565. }
  566. }
  567.  
  568. function hasAttribute(obj, key) {
  569. return obj && obj.hasAttribute(key);
  570. }
  571.  
  572. function isTheater() {
  573. const cssElm = kRef(ytdFlexy);
  574. return (cssElm && cssElm.hasAttribute('theater'))
  575. }
  576.  
  577. function isFullScreen() {
  578. const cssElm = kRef(ytdFlexy);
  579. return (cssElm && cssElm.hasAttribute('fullscreen'))
  580. }
  581.  
  582. function isChatExpand() {
  583. const cssElm = kRef(ytdFlexy);
  584. return cssElm && cssElm.hasAttribute('userscript-chatblock') && !cssElm.hasAttribute('userscript-chat-collapsed')
  585. }
  586.  
  587. function isWideScreenWithTwoColumns() {
  588. const cssElm = kRef(ytdFlexy);
  589. return (cssElm && cssElm.hasAttribute('is-two-columns_'))
  590. }
  591.  
  592. function isAnyActiveTab() {
  593. return $('#right-tabs .tab-btn.active').length > 0
  594. }
  595.  
  596. function isEngagementPanelExpanded() {
  597. const cssElm = kRef(ytdFlexy);
  598. return (cssElm && +cssElm.getAttribute('userscript-engagement-panel') > 0)
  599. }
  600.  
  601. function engagement_panels_() {
  602.  
  603. let res = [];
  604.  
  605. let v = 0,
  606. k = 1,
  607. count = 0;
  608. for (const ePanel of document.querySelectorAll('ytd-watch-flexy ytd-engagement-panel-section-list-renderer[tabview-attr-checked]')) {
  609.  
  610. let visibility = ePanel.getAttribute('visibility') //ENGAGEMENT_PANEL_VISIBILITY_EXPANDED //ENGAGEMENT_PANEL_VISIBILITY_HIDDEN
  611.  
  612. switch (visibility) {
  613. case 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED':
  614. v |= k;
  615. count++;
  616. res.push({ ePanel, k, visible: true });
  617. break;
  618. case 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN':
  619. res.push({ ePanel, k, visible: false });
  620. break;
  621. default:
  622. res.push({ ePanel, k, visible: false });
  623. }
  624.  
  625. k = k << 1;
  626.  
  627. }
  628. return { list: res, value: v, count: count };
  629. }
  630.  
  631.  
  632. function ytBtnOpenEngagementPanel(panel_id) {
  633.  
  634. if (typeof panel_id == 'string') {
  635. panel_id = panel_id.replace('#engagement-panel-', '');
  636. panel_id = parseInt(panel_id);
  637. }
  638. if (panel_id >= 0) {} else return false;
  639.  
  640. let panels = engagement_panels_();
  641.  
  642. for (const { ePanel, k, visible } of panels.list) {
  643. if ((panel_id & k) === k) {
  644. if (!visible) ePanel.setAttribute('visibility', "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED");
  645. } else {
  646. if (visible) ytBtnCloseEngagementPanel(ePanel);
  647. }
  648. }
  649.  
  650. }
  651.  
  652. function ytBtnCloseEngagementPanel(s) {
  653. //ePanel.setAttribute('visibility',"ENGAGEMENT_PANEL_VISIBILITY_HIDDEN");
  654. let btn = s.querySelector('ytd-watch-flexy ytd-engagement-panel-title-header-renderer #header>#visibility-button>ytd-button-renderer');
  655. if (btn) {
  656. btn.click();
  657. }
  658. }
  659.  
  660. function ytBtnCloseEngagementPanels() {
  661. if (isEngagementPanelExpanded()) {
  662. for (const s of document.querySelectorAll('ytd-watch-flexy ytd-engagement-panel-section-list-renderer[tabview-attr-checked]')) {
  663. if (s.getAttribute('visibility') == "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED") ytBtnCloseEngagementPanel(s);
  664. }
  665. }
  666. }
  667.  
  668. function ytBtnSetTheater() {
  669. if (!isTheater()) {
  670. const sizeBtn = document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
  671. if (sizeBtn) sizeBtn.click();
  672. }
  673. }
  674.  
  675. function ytBtnCancelTheater() {
  676. if (isTheater()) {
  677. const sizeBtn = document.querySelector('ytd-watch-flexy #ytd-player button.ytp-size-button')
  678. if (sizeBtn) sizeBtn.click();
  679. }
  680. }
  681.  
  682. function ytBtnExpandChat() {
  683. let button = document.querySelector('ytd-live-chat-frame#chat[collapsed]>.ytd-live-chat-frame#show-hide-button')
  684. if (button) button.querySelector('ytd-toggle-button-renderer').click();
  685. }
  686.  
  687. function ytBtnCollapseChat() {
  688. let button = document.querySelector('ytd-live-chat-frame#chat:not([collapsed])>.ytd-live-chat-frame#show-hide-button')
  689. if (button) button.querySelector('ytd-toggle-button-renderer').click();
  690. }
  691.  
  692.  
  693. function hackImgShadow(imgShadow) {
  694. // add to #columns and add back after loaded
  695. let img = imgShadow.querySelector('img')
  696. if (!img) return;
  697.  
  698. let p = imgShadow.parentNode
  699. let z = $(imgShadow).clone()[0]; //to occupy the space
  700. p.replaceChild(z, imgShadow)
  701. $(imgShadow).prependTo('#columns'); // refer to css hack
  702.  
  703. function onload(evt) {
  704. if (evt) this.removeEventListener('load', onload, false)
  705. p.replaceChild(imgShadow, z)
  706. p = null;
  707. z = null;
  708. imgShadow = null;
  709. }
  710.  
  711. if (img.complete) onload();
  712. else img.addEventListener('load', onload, false)
  713. }
  714.  
  715.  
  716. const Q = {}
  717. const FOnce = {}
  718.  
  719. const $callOnceAsync = async function(key) {
  720. if (FOnce[key] && FOnce[key]() === false) FOnce[key] = null
  721. }
  722.  
  723.  
  724.  
  725. function chatFrameContentDocument() {
  726. // non-null if iframe exist && contentDocument && readyState = complete
  727.  
  728. let iframe = document.querySelector('ytd-live-chat-frame iframe#chatframe');
  729. if (!iframe) return null; //iframe must be there
  730. let cDoc = null;
  731. try {
  732. cDoc = iframe.contentDocument;
  733. } catch (e) {}
  734. if (!cDoc) return null;
  735. if (cDoc.readyState != 'complete') return null; //we must wait for its completion
  736.  
  737. return cDoc;
  738.  
  739. }
  740.  
  741. function chatFrameElement(cssSelector) {
  742. let cDoc = chatFrameContentDocument();
  743. if (!cDoc) return null;
  744. let elm = null;
  745. try {
  746. elm = cDoc.querySelector(cssSelector)
  747. } catch (e) {
  748. console.log('iframe error', e)
  749. }
  750. return elm;
  751. }
  752.  
  753.  
  754.  
  755.  
  756. function fixTabs() {
  757.  
  758.  
  759. if (!scriptEnable) return;
  760.  
  761.  
  762. let queryElement = document.querySelector('*:not(#tab-videos)>#related:not([non-placeholder-videos]) > ytd-watch-next-secondary-results-renderer')
  763.  
  764. let isRelocated = !!queryElement;
  765.  
  766.  
  767.  
  768. if (isRelocated) {
  769.  
  770. let relocatedRelated = queryElement.parentNode; // NOT NULL
  771.  
  772. let right_tabs = document.querySelector('#right-tabs');
  773. let tab_videos = right_tabs.querySelector("#tab-videos");
  774.  
  775. if (!right_tabs || !tab_videos) return;
  776.  
  777. for (const s of relocatedRelated.querySelectorAll('#related')) {
  778. s.setAttribute('non-placeholder-videos', '')
  779. }
  780.  
  781. let target_container = document.querySelector('ytd-watch-flexy:not([is-two-columns_]) #primary-inner, ytd-watch-flexy[is-two-columns_] #secondary-inner')
  782.  
  783. if (target_container) target_container.append(right_tabs) // last-child
  784.  
  785.  
  786. let videos_related = relocatedRelated; // NOT NULL
  787. $('[placeholder-videos]').removeAttr('placeholder-videos');
  788. $('[placeholder-for-youtube-play-next-queue]').removeAttr('placeholder-for-youtube-play-next-queue');
  789.  
  790. tab_videos.appendChild(videos_related);
  791. let videos_results_renderer = relocatedRelated.querySelector("ytd-watch-next-secondary-results-renderer");
  792. if (videos_results_renderer) videos_results_renderer.setAttribute('data-dom-changed-by-tabview-youtube', scriptVersionForExternal);
  793. videos_related.setAttribute('placeholder-for-youtube-play-next-queue', '')
  794. videos_related.setAttribute('placeholder-videos', '')
  795.  
  796. $('[placeholder-videos]').on("scroll", makeBodyScrollByEvt);
  797.  
  798.  
  799.  
  800.  
  801. }
  802.  
  803.  
  804.  
  805.  
  806. let chatroom = null;
  807. if (chatroom = document.querySelector('*:not([data-positioner="before|#chat"]) + ytd-live-chat-frame#chat, ytd-live-chat-frame#chat:first-child')) {
  808.  
  809. let positioner = document.querySelector('tabview-youtube-positioner[data-positioner="before|#chat"]');
  810. if (positioner) positioner.remove();
  811.  
  812.  
  813. if (document.querySelector('.YouTubeLiveFilledUpView')) {
  814. // no relocation
  815. } else {
  816.  
  817. $(chatroom).insertBefore('#right-tabs')
  818.  
  819. }
  820.  
  821.  
  822. $(positioner ? positioner : document.createElement('tabview-youtube-positioner')).attr('data-positioner', 'before|#chat').insertBefore(chatroom)
  823.  
  824.  
  825. }
  826.  
  827.  
  828. }
  829.  
  830. const injectionScript_fixAutoComplete = function() {
  831.  
  832. // https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.min.js
  833.  
  834. for (const s of document.querySelectorAll('[autocomplete="off"]:not([data-autocomplete-results-id])')) {
  835.  
  836.  
  837. let sc = s.sc;
  838. if (sc instanceof HTMLElement) {
  839.  
  840. let id = Date.now();
  841. s.setAttribute('data-autocomplete-results-id', id);
  842. sc.setAttribute('data-autocomplete-input-id', id);
  843.  
  844. if (window.WeakRef) {
  845. s._sc = new WeakRef(sc);
  846. s.sc = null;
  847. delete s.sc;
  848. Object.defineProperty(s, 'sc', {
  849. get: function() { return s._sc.deref() || null; },
  850. enumerable: true,
  851. configurable: true
  852. })
  853. }
  854.  
  855. if (sc.hasAttribute('autocomplete-disable-updatesc') && typeof s.updateSC == 'function') {
  856.  
  857. window.removeEventListener('resize', s.updateSC);
  858. s.updateSC = function() {};
  859.  
  860. }
  861.  
  862. sc.dispatchEvent(new CustomEvent('autocomplete-sc-exist'));
  863.  
  864.  
  865. }
  866.  
  867. }
  868.  
  869. };
  870.  
  871.  
  872. function handlerAutoCompleteExist() {
  873.  
  874.  
  875. let autoComplete = this;
  876.  
  877. autoComplete.removeEventListener('autocomplete-sc-exist', handlerAutoCompleteExist, false)
  878.  
  879. let domId = autoComplete.getAttribute('data-autocomplete-input-id')
  880. let searchBox = autoComplete.ownerDocument.querySelector(`[data-autocomplete-results-id="${domId}"]`)
  881.  
  882. if (!domId || !searchBox) return;
  883.  
  884. let positioner = searchBox.nextSibling;
  885. if (positioner && positioner.nodeName.toLowerCase() == "autocomplete-positioner") {} else if (positioner && positioner.nodeName.toLowerCase() != "autocomplete-positioner") {
  886. $(positioner = document.createElement("autocomplete-positioner")).insertAfter(searchBox);
  887. } else {
  888. $(positioner = document.createElement("autocomplete-positioner")).prependTo(searchBox.parentNode);
  889. }
  890. $(autoComplete).prependTo(positioner);
  891.  
  892. positioner.style.setProperty('--sb-margin-bottom', getComputedStyle(searchBox).marginBottom)
  893. positioner.style.setProperty('--height', searchBox.offsetHeight + 'px')
  894.  
  895. }
  896.  
  897. function mtf_fixAutoCompletePosition(elmAutoComplete) {
  898.  
  899. elmAutoComplete.setAttribute('autocomplete-disable-updatesc', '')
  900. elmAutoComplete.addEventListener('autocomplete-sc-exist', handlerAutoCompleteExist, false)
  901.  
  902. if (isMyScriptInChromeRuntime())
  903. addScriptByURL(window.chrome.runtime.getURL('js/injectionScript_fixAutoComplete.js'));
  904. else
  905. addScript(`!!(${injectionScript_fixAutoComplete+''})()`);
  906. // addScriptByURL(`${githubURLBase}/${githubURLCommit}/js/injectionScript_fixAutoComplete.js`);
  907.  
  908. }
  909.  
  910. function mtf_AfterFixTabs() {
  911.  
  912.  
  913. let ytdFlexyElm = kRef(ytdFlexy);
  914. if (!scriptEnable || !ytdFlexyElm) return;
  915.  
  916. const rootElement = Q.mutationTarget || ytdFlexyElm;
  917.  
  918.  
  919.  
  920. 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])')
  921.  
  922. if (autocomplete) {
  923.  
  924. const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  925.  
  926.  
  927. if (searchBox) {
  928.  
  929.  
  930. autocomplete.parentNode.setAttribute('position-fixed-by-tabview-youtube', '');
  931. autocomplete.setAttribute('position-fixed-by-tabview-youtube', '');
  932. autocomplete.setAttribute('userscript-scrollbar-render', '')
  933.  
  934. if (!searchBox.hasAttribute('is-set-click-to-toggle')) {
  935. searchBox.setAttribute('is-set-click-to-toggle', '')
  936. searchBox.addEventListener('click', function() {
  937.  
  938.  
  939. setTimeout(() => {
  940. const autocomplete = document.querySelector(`.autocomplete-suggestions[data-autocomplete-input-id="${ this.getAttribute('data-autocomplete-results-id') }"]`)
  941.  
  942. if (!autocomplete) return;
  943.  
  944. const isNotEmpty = (autocomplete.textContent || '').length > 0 && (this.value || '').length > 0;
  945.  
  946. if (isNotEmpty) {
  947.  
  948. let elmVisible = isDOMVisible(autocomplete)
  949.  
  950. if (elmVisible) $(autocomplete).hide();
  951. else $(autocomplete).show();
  952.  
  953. }
  954.  
  955. }, 20);
  956.  
  957. })
  958.  
  959. let timeoutOnce_searchbox_keyup = new Timeout();
  960. searchBox.addEventListener('keyup', function() {
  961.  
  962. timeoutOnce_searchbox_keyup.set(() => {
  963.  
  964. const autocomplete = document.querySelector(`.autocomplete-suggestions[data-autocomplete-input-id="${ this.getAttribute('data-autocomplete-results-id') }"]`)
  965.  
  966. if (!autocomplete) return;
  967.  
  968.  
  969. const isNotEmpty = (autocomplete.textContent || '').length > 0 && (this.value || '').length > 0
  970.  
  971. if (isNotEmpty) {
  972.  
  973. let elmVisible = isDOMVisible(autocomplete)
  974.  
  975. if (!elmVisible) $(autocomplete).show();
  976.  
  977. }
  978.  
  979. }, 20);
  980.  
  981. })
  982.  
  983. }
  984.  
  985.  
  986.  
  987. }
  988.  
  989. }
  990.  
  991.  
  992.  
  993.  
  994. let currentLastVideo = rootElement.querySelector('[placeholder-videos] #items ytd-compact-video-renderer:last-of-type')
  995. let prevLastVideo = kRef(_cachedLastVideo);
  996.  
  997. if (prevLastVideo !== currentLastVideo && currentLastVideo) {
  998. _cachedLastVideo = mWeakRef(currentLastVideo);
  999. }
  1000.  
  1001. if (prevLastVideo !== currentLastVideo && currentLastVideo && prevLastVideo) {
  1002.  
  1003. let isPrevRemoved = !prevLastVideo.parentNode
  1004.  
  1005.  
  1006. function getVideoListHash() {
  1007.  
  1008. let res = [...document.querySelectorAll('[placeholder-videos] #items ytd-compact-video-renderer')].map(renderer => {
  1009. return renderer.querySelector('a[href*="watch"][href*="v="]').getAttribute('href')
  1010.  
  1011. }).join('|')
  1012. // /watch?v=XXXXX|/watch?v=XXXXXX|/watch?v=XXXXXX
  1013.  
  1014. // alternative - DOM.data.videoId
  1015. // let elms = document.querySelectorAll('[placeholder-videos] #items ytd-compact-video-renderer')
  1016. // let res = [...elms].map(elm=>elm.data.videoId||'').join('|') ;
  1017.  
  1018. if (res.indexOf('||') >= 0) {
  1019. res = '';
  1020. }
  1021.  
  1022. return res ? res : null;
  1023. }
  1024.  
  1025. if (isPrevRemoved) {
  1026.  
  1027. // this is the replacement of videos instead of addition
  1028.  
  1029. const searchBox = document.querySelector('[placeholder-for-youtube-play-next-queue] input#suggestions-search')
  1030.  
  1031. let currentPlayListHash = getVideoListHash() || null;
  1032.  
  1033. if (!currentPlayListHash) {
  1034.  
  1035. } else if (!videoListBeforeSearch && searchBox) {
  1036.  
  1037. videoListBeforeSearch = currentPlayListHash;
  1038. if (videoListBeforeSearch) {
  1039. //console.log('fromSearch', videoListBeforeSearch)
  1040.  
  1041. requestAnimationFrame(function() {
  1042.  
  1043. let renderer = document.querySelector('[placeholder-videos] ytd-watch-next-secondary-results-renderer');
  1044. if (searchBox && searchBox.parentNode) searchBox.blur();
  1045.  
  1046. if (renderer) {
  1047. let scrollParent = renderer.parentNode;
  1048. if (scrollParent.scrollHeight > scrollParent.offsetHeight) {
  1049. let targetTop = renderer.offsetTop;
  1050. if (searchBox && searchBox.parentNode == scrollParent) targetTop -= searchBox.offsetHeight
  1051. scrollParent.scrollTop = targetTop - scrollParent.firstChild.offsetTop;
  1052. }
  1053. }
  1054.  
  1055. });
  1056.  
  1057. }
  1058.  
  1059. } else if (videoListBeforeSearch) {
  1060.  
  1061. if (currentPlayListHash != videoListBeforeSearch) {
  1062.  
  1063. videoListBeforeSearch = null;
  1064. //console.log('fromSearch', videoListBeforeSearch)
  1065.  
  1066.  
  1067. }
  1068.  
  1069. }
  1070.  
  1071.  
  1072. }
  1073.  
  1074.  
  1075. }
  1076.  
  1077.  
  1078.  
  1079.  
  1080. }
  1081.  
  1082. function base_ChatExist() {
  1083.  
  1084. let ytdFlexyElm = kRef(ytdFlexy);
  1085. if (!scriptEnable || !ytdFlexyElm) return null;
  1086.  
  1087. // no mutation triggering if the changes are inside the iframe
  1088.  
  1089. // 1) Detection of #continuations inside iframe
  1090. // iframe ownerDocument is accessible due to same origin
  1091. // if the chatroom is collasped, no determination of live chat or replay (as no #continuations and somehow a blank iframe doc)
  1092.  
  1093. // 2) Detection of meta tag
  1094. // This is fastest but not reliable. It is somehow a bug that the navigation might not update the meta tag content
  1095.  
  1096. // 3) Detection of HTMLElement inside video player for live video
  1097.  
  1098. // (1)+(3) = solution
  1099.  
  1100. let attr_chatblock = null
  1101. let attr_chatcollapsed = null;
  1102.  
  1103. const elmChat = document.querySelector('ytd-live-chat-frame#chat')
  1104. let elmCont = null;
  1105. if (elmChat) {
  1106. elmCont = chatFrameElement('yt-live-chat-renderer #continuations')
  1107.  
  1108. let s = 0;
  1109. if (elmCont) {
  1110. //not found if it is collasped.
  1111. s |= elmCont.querySelector('yt-timed-continuation') ? 1 : 0;
  1112. s |= elmCont.querySelector('yt-live-chat-replay-continuation, yt-player-seek-continuation') ? 2 : 0;
  1113. //s |= elmCont.querySelector('yt-live-chat-restricted-participation-renderer')?4:0;
  1114. if (s == 1) {
  1115. //console.log(7005)
  1116. attr_chatblock = 'chat-live';
  1117. //disableComments_LiveChat();
  1118. }
  1119. if (s == 2) attr_chatblock = 'chat-playback';
  1120. //if(s==5) attr_chatblock='chat-live-paid';
  1121.  
  1122. if (s == 1) $("span#tab3-txt-loader").text('');
  1123.  
  1124. }
  1125. //keep unknown as original
  1126.  
  1127. } else {
  1128. attr_chatblock = false;
  1129. attr_chatcollapsed = false;
  1130.  
  1131. }
  1132.  
  1133. return { attr_chatblock, attr_chatcollapsed }
  1134.  
  1135. }
  1136.  
  1137.  
  1138. function mtf_ChatExist() {
  1139.  
  1140. let ytdFlexyElm = kRef(ytdFlexy);
  1141. if (!scriptEnable || !ytdFlexyElm) return;
  1142.  
  1143. const elmChat = document.querySelector('ytd-live-chat-frame#chat')
  1144. let elmCont = null;
  1145. if (elmChat) {
  1146. elmCont = chatFrameElement('yt-live-chat-renderer #continuations')
  1147. }
  1148.  
  1149. const chatBlockR = (elmChat ? 1 : 0) + (elmCont ? 2 : 0)
  1150. if (Q.mtf_chatBlockQ !== chatBlockR) {
  1151. Q.mtf_chatBlockQ = chatBlockR
  1152. let rChatExist = base_ChatExist();
  1153. if (rChatExist) {
  1154. let { attr_chatblock, attr_chatcollapsed } = rChatExist;
  1155. wAttr(ytdFlexyElm, 'userscript-chatblock', attr_chatblock)
  1156. wAttr(ytdFlexyElm, 'userscript-chat-collapsed', attr_chatcollapsed)
  1157. }
  1158. }
  1159. }
  1160.  
  1161.  
  1162.  
  1163.  
  1164. let lastScrollAt1 = 0;
  1165.  
  1166. function makeBodyScrollByEvt() {
  1167. let ct = Date.now();
  1168. if (ct - lastScrollAt1 < 6) return; // avoid duplicate calling
  1169. lastScrollAt1 = ct;
  1170. window.dispatchEvent(new Event("scroll")); // dispatch Scroll Event to Window for content display
  1171. }
  1172.  
  1173. let lastScrollAt2 = 0;
  1174.  
  1175. function makeBodyScroll() {
  1176. let ct = Date.now();
  1177. if (ct - lastScrollAt2 < 30) return; // avoid over triggering
  1178. lastScrollAt2 = ct;
  1179. requestAnimationFrame(() => {
  1180. window.dispatchEvent(new Event("scroll")); // ask youtube to display content
  1181. })
  1182. }
  1183.  
  1184. let requestingComments = null
  1185. let scrollForComments_lastStart = 0;
  1186.  
  1187. function scrollForComments_TF() {
  1188. let comments = requestingComments;
  1189. if (!comments) return;
  1190. if (comments.hasAttribute('hidden')) {
  1191. window.dispatchEvent(new Event("scroll"));
  1192. } else requestingComments = null;
  1193. }
  1194.  
  1195. function scrollForComments() {
  1196. scrollForComments_TF();
  1197. if (!requestingComments) return;
  1198. requestAnimationFrame(scrollForComments_TF);
  1199. let ct = Date.now();
  1200. if (ct - scrollForComments_lastStart < 60) return;
  1201. scrollForComments_lastStart = ct;
  1202. setTimeout(scrollForComments_TF, 80);
  1203. setTimeout(scrollForComments_TF, 240);
  1204. setTimeout(scrollForComments_TF, 870);
  1205. }
  1206.  
  1207.  
  1208.  
  1209.  
  1210. const mtoCs = { mtoNav: null, mtoBody: null };
  1211.  
  1212.  
  1213. const mtoVs = {}
  1214.  
  1215. const mutation_target_id_list = ['ytp-caption-window-container', 'items', 'button', 'movie_player', 'player-ads', 'hover-overlays', 'replies'];
  1216. const mutation_target_class_list = ['ytp-panel-menu', 'ytp-endscreen-content'];
  1217.  
  1218. function isMtoOverallSkip(dTarget) {
  1219.  
  1220. if (!dTarget || dTarget.nodeType !== 1) return true;
  1221.  
  1222. if (mutation_target_id_list.includes(dTarget.id)) return true;
  1223.  
  1224. let className = dTarget.className.toLowerCase();
  1225. let classNameSplit = className.split(' ');
  1226. for (const c of classNameSplit) {
  1227. if (mutation_target_class_list.includes(c)) return true;
  1228. }
  1229.  
  1230. return false;
  1231. }
  1232.  
  1233.  
  1234. const mutation_div_id_ignorelist = [
  1235. 'metadata-line',
  1236. 'ytp-caption-window-container',
  1237. 'top-level-buttons-computed',
  1238. 'microformat',
  1239. 'visibility-button',
  1240. 'info-strings',
  1241. 'action-menu',
  1242. 'reply-button-end'
  1243. ];
  1244.  
  1245. const mutation_div_class_ignorelist = [
  1246. 'badge', 'tp-yt-paper-tooltip', 'ytp-autonav-endscreen-upnext-header',
  1247. 'ytp-bound-time-left', 'ytp-bound-time-right', 'ytp-share-icon',
  1248. 'ytp-tooltip-title', 'annotation', 'ytp-copylink-icon', 'ytd-thumbnail',
  1249. 'paper-ripple',
  1250. //caption
  1251. 'captions-text', 'caption-visual-line', 'ytp-caption-segment', 'ytp-caption-window-container',
  1252. //menu
  1253. 'ytp-playlist-menu-button-text',
  1254.  
  1255. 'ytp-bezel-icon', 'ytp-bezel-text',
  1256. 'dropdown-content',
  1257. 'tp-yt-paper-menu-button', 'tp-yt-iron-dropdown',
  1258.  
  1259. 'ytd-metadata-row-renderer', // #content.ytd-metadata-row-renderer inside each of ytd-metadata-row-renderer (ytd-expander)
  1260. '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
  1261.  
  1262. 'autocomplete-suggestions' // autocomplete-suggestions
  1263. ];
  1264.  
  1265. const mutation_target_tag_ignorelist = [
  1266. 'ytd-channel-name', 'tp-yt-iron-dropdown', 'tp-yt-paper-tooltip',
  1267. 'tp-yt-paper-listbox', 'yt-img-shadow', 'ytd-thumbnail', 'ytd-video-meta-block',
  1268.  
  1269. 'yt-icon-button', 'tp-yt-paper-button', 'yt-formatted-string', 'yt-icon', 'button', 'paper-ripple',
  1270.  
  1271. 'ytd-player-microformat-renderer',
  1272. 'ytd-engagement-panel-section-list-renderer', 'ytd-engagement-panel-title-header-renderer',
  1273. 'ytd-comment-renderer', 'ytd-menu-renderer', 'ytd-badge-supported-renderer',
  1274. 'ytd-subscribe-button-renderer', 'ytd-subscription-notification-toggle-button-renderer',
  1275. 'ytd-button-renderer', 'ytd-toggle-button-renderer',
  1276. 'yt-pdg-comment-chip-renderer', 'ytd-comment-action-buttons-renderer', 'ytd-comment-thread-renderer',
  1277. 'ytd-compact-radio-renderer', 'ytd-compact-video-renderer',
  1278. 'ytd-video-owner-renderer',
  1279. 'ytd-metadata-row-renderer', //ytd-metadata-row-renderer is part of the #collapsible inside ytd-expander
  1280.  
  1281. 'ytd-moving-thumbnail-renderer',
  1282. 'ytd-thumbnail-overlay-toggle-button-renderer',
  1283. 'ytd-thumbnail-overlay-bottom-panel-renderer', 'ytd-thumbnail-overlay-equalizer',
  1284. 'ytd-thumbnail-overlay-now-playing-renderer', 'ytd-thumbnail-overlay-resume-playback-renderer',
  1285. 'ytd-thumbnail-overlay-side-panel-renderer', 'ytd-thumbnail-overlay-time-status-renderer',
  1286. 'ytd-thumbnail-overlay-hover-text-renderer',
  1287.  
  1288. 'yt-interaction',
  1289. 'tp-yt-paper-spinner-lite', 'tp-yt-paper-spinner',
  1290.  
  1291. 'h1', 'h2', 'h3', 'h4', 'h5', 'span', 'a',
  1292.  
  1293. 'meta', 'br', 'script', 'style', 'link', 'dom-module', 'template'
  1294. ];
  1295.  
  1296. function isMtoTargetSkip(mutation) {
  1297. //skip not important mutation tartget
  1298.  
  1299. if (!mutation) return true;
  1300. let { type, target } = mutation
  1301.  
  1302. if (!target || target.nodeType !== 1 || type != 'childList') return true;
  1303.  
  1304. let tagName = target.nodeName.toLowerCase();
  1305. let className;
  1306. let classNameSplit;
  1307.  
  1308. if (mutation_target_tag_ignorelist.includes(tagName)) return true;
  1309.  
  1310. switch (tagName) {
  1311.  
  1312.  
  1313. case 'ytd-expander':
  1314. if (target.id == 'expander' && Q.comments_section_loaded == 1 && target.classList.contains('ytd-comment-renderer')) return true; // load comments
  1315. return false;
  1316.  
  1317. case 'div':
  1318.  
  1319. if (target.id == 'contents') {
  1320. return false;
  1321. }
  1322. if (mutation_div_id_ignorelist.includes(target.id)) return true;
  1323.  
  1324. className = target.className.toLowerCase();
  1325. classNameSplit = className.split(' ');
  1326. for (const c of classNameSplit) {
  1327. if (mutation_div_class_ignorelist.includes(c)) return true;
  1328. }
  1329.  
  1330. return false;
  1331.  
  1332. }
  1333.  
  1334. return false;
  1335.  
  1336. }
  1337.  
  1338.  
  1339.  
  1340. // continuous check for element relocation
  1341. function mtf_append_comments() {
  1342.  
  1343. let ytdFlexyElm = kRef(ytdFlexy);
  1344. if (!scriptEnable || !ytdFlexyElm) return;
  1345.  
  1346. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1347.  
  1348. let comments = rootElement.querySelector('#primary ytd-watch-metadata ~ ytd-comments#comments');
  1349. if (comments) $(comments).appendTo('#tab-comments').attr('data-dom-changed-by-tabview-youtube', scriptVersionForExternal)
  1350. }
  1351.  
  1352. // continuous check for element relocation
  1353. function mtf_liveChatBtnF() {
  1354. let ytdFlexyElm = kRef(ytdFlexy);
  1355. if (!scriptEnable || !ytdFlexyElm) return;
  1356.  
  1357. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1358.  
  1359. let button = rootElement.querySelector('ytd-live-chat-frame#chat>.ytd-live-chat-frame#show-hide-button:nth-child(n+2)');
  1360. if (button) button.parentNode.insertBefore(button, button.parentNode.firstChild)
  1361. }
  1362.  
  1363.  
  1364.  
  1365. // continuous check for element relocation
  1366. // fired at begining & window resize, etc
  1367. function mtf_append_playlist() {
  1368.  
  1369. let ytdFlexyElm = kRef(ytdFlexy);
  1370. if (!scriptEnable || !ytdFlexyElm) return;
  1371.  
  1372. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1373.  
  1374. let ple1 = rootElement.querySelector("*:not(#ytd-userscript-playlist)>ytd-playlist-panel-renderer#playlist");
  1375. if (ple1) {
  1376. let ct = Date.now();
  1377. let truePlaylist = null;
  1378. let truePlaylist_items = document.querySelector('ytd-playlist-panel-renderer#playlist #items:not(:empty)');
  1379. if (truePlaylist_items) {
  1380.  
  1381. let pElm = truePlaylist_items.parentNode;
  1382. while (pElm && pElm.nodeType === 1) {
  1383. if (pElm.id == 'playlist') {
  1384. pElm.setAttribute('tabview-true-playlist', ct)
  1385. truePlaylist = pElm;
  1386. break;
  1387. }
  1388. pElm = pElm.parentNode;
  1389. }
  1390.  
  1391. }
  1392.  
  1393. if (!truePlaylist) truePlaylist = ple1; // NOT NULL
  1394.  
  1395. for (const s of document.querySelectorAll(`*:not(#ytd-userscript-playlist)>ytd-playlist-panel-renderer#playlist:not([tabview-true-playlist="${ct}"])`))
  1396. s.parentNode.removeChild(s);
  1397.  
  1398. let $wrapper = getWrapper('ytd-userscript-playlist')
  1399. $wrapper.append(truePlaylist).appendTo(document.querySelector("#tab-list"));
  1400. truePlaylist.setAttribute('data-dom-changed-by-tabview-youtube', scriptVersionForExternal)
  1401. setDisplayedPlaylist(); // relocation after re-layout
  1402.  
  1403. requestAnimationFrame(() => {
  1404. let ytdFlexyElm = kRef(ytdFlexy);
  1405. if (!scriptEnable || !ytdFlexyElm) return;
  1406. if (!switchTabActivity_lastTab && (ytdFlexyElm.getAttribute('tabview-selection') + '').indexOf('#tab-') === 0 && /https\:\/\/www\.youtube\.com\/watch.*[\?\&]list=[\w\-\_]+/.test(location.href)) {
  1407. if (setToActiveTab('#tab-list')) switchTabActivity_lastTab = '#tab-list';
  1408. }
  1409. })
  1410.  
  1411. }
  1412. }
  1413.  
  1414.  
  1415. // content fix - info & playlist
  1416. // fired at begining, and keep for in case any change
  1417. function mtf_fix_details() {
  1418.  
  1419. if (!scriptEnable) return;
  1420.  
  1421. if (no_fix_contents_until < Date.now()) {
  1422. const content = document.querySelector('#meta-contents ytd-expander>#content, #tab-info ytd-expander>#content')
  1423. if (content) {
  1424. no_fix_contents_until = Date.now() + 3000;
  1425. setTimeout(function() {
  1426. const expander = content.parentNode;
  1427.  
  1428. if (expander.hasAttribute('collapsed')) wAttr(expander, 'collapsed', false);
  1429. expander.style.setProperty('--ytd-expander-collapsed-height', '');
  1430.  
  1431. let btn1 = expander.querySelector('tp-yt-paper-button#less:not([hidden])');
  1432. let btn2 = expander.querySelector('tp-yt-paper-button#more:not([hidden])');
  1433.  
  1434. if (btn1) wAttr(btn1, 'hidden', true);
  1435. if (btn2) wAttr(btn2, 'hidden', true);
  1436. }, 40);
  1437.  
  1438. }
  1439. }
  1440.  
  1441. if (no_fix_playlist_until < Date.now()) {
  1442. // just in case the playlist is collapsed
  1443. const playlist = document.querySelector('#tab-list ytd-playlist-panel-renderer#playlist')
  1444. if (playlist) {
  1445. no_fix_playlist_until = Date.now() + 3000;
  1446. setTimeout(function() {
  1447. if (playlist.hasAttribute('collapsed')) wAttr(playlist, 'collapsed', false);
  1448. if (playlist.hasAttribute('collapsible')) wAttr(playlist, 'collapsible', false);
  1449. }, 40)
  1450. }
  1451. }
  1452.  
  1453.  
  1454. }
  1455.  
  1456.  
  1457.  
  1458. function status_commentsHidden() {
  1459.  
  1460. var comments = document.querySelector('ytd-comments#comments')
  1461. if (!comments || (comments.childElementCount === 0 && comments.hasAttribute('hidden'))) return 2;
  1462. if (comments.hasAttribute('hidden')) return 1;
  1463. return 0;
  1464.  
  1465. }
  1466.  
  1467.  
  1468. function getFMT(ytdFlexyElm) {
  1469.  
  1470.  
  1471. let fmt = ["ytd-comments#comments #count.ytd-comments-header-renderer", 'ytd-item-section-renderer#sections #header ~ #contents>ytd-message-renderer'].map(s => {
  1472.  
  1473. let elm = ytdFlexyElm.querySelector(s)
  1474. if (!elm) return null;
  1475. return elm.querySelector('yt-formatted-string') || elm.closest('yt-formatted-string') || null
  1476.  
  1477.  
  1478. });
  1479.  
  1480. return fmt
  1481. }
  1482.  
  1483. function toFST(elm) {
  1484.  
  1485. if (!elm) return null;
  1486. return elm.querySelector('yt-formatted-string') || elm.closest('yt-formatted-string') || null
  1487. }
  1488.  
  1489.  
  1490. function innerCommentsLoader() {
  1491.  
  1492.  
  1493. const rootElement = Q.mutationTarget || kRef(ytdFlexy);
  1494. if (!rootElement) return;
  1495.  
  1496. let messageElm, messageStr, commentRenderer;
  1497.  
  1498. if (commentRenderer = rootElement.querySelector("ytd-comments#comments #count.ytd-comments-header-renderer")) {
  1499.  
  1500. return {
  1501. status: 1,
  1502. f: () => {
  1503.  
  1504. let span = document.querySelector("span#tab3-txt-loader")
  1505. Q.fetchedOnce = true;
  1506. let r = '0';
  1507. let txt = commentRenderer.textContent
  1508. if (typeof txt == 'string') {
  1509. let m = txt.match(/[\d\,\s]+/)
  1510. if (m) {
  1511. r = m[0].trim()
  1512.  
  1513.  
  1514. }
  1515. }
  1516. if (span) span.textContent = r;
  1517. mtoInterval = mtoInterval2;
  1518. setCommentSection(1);
  1519. cmItem = mWeakRef(toFST(commentRenderer))
  1520. comments_section_loaded_elm = mWeakRef(document.querySelector('ytd-comments#comments div#contents'))
  1521. }
  1522. }
  1523.  
  1524. } else if ((messageElm = rootElement.querySelector('ytd-item-section-renderer#sections #header ~ #contents>ytd-message-renderer')) && (messageStr = (messageElm.textContent || '').trim())) { //ytd-message-renderer
  1525. // it is possible to get the message before the header generation.
  1526. return {
  1527. status: 2,
  1528. f: () => {
  1529. setTimeout(function() {
  1530. if (Q.fetchedOnce) return;
  1531. let span = document.querySelector("span#tab3-txt-loader")
  1532. const mainMsg = messageElm.querySelector('#message, #submessage')
  1533. if (mainMsg && mainMsg.textContent) {
  1534. for (const msg of mainMsg.querySelectorAll('*:not(:empty)')) {
  1535. if (msg.childElementCount === 0 && msg.textContent) {
  1536. messageStr = msg.textContent.trim()
  1537. break
  1538. }
  1539. }
  1540. }
  1541. if (span) span.textContent = messageStr;
  1542. mtoInterval = mtoInterval2;
  1543. setCommentSection(1);
  1544. cmItem = mWeakRef(toFST(messageElm))
  1545. comments_section_loaded_elm = mWeakRef(document.querySelector('ytd-comments#comments div#contents'))
  1546. }, 40);
  1547. }
  1548. }
  1549.  
  1550. }
  1551.  
  1552. }
  1553.  
  1554. const FP = {
  1555.  
  1556. mtoNav_delayedF: () => {
  1557.  
  1558. let { addP, removeP, mutationTarget } = Q;
  1559.  
  1560. Q.addP = 0;
  1561. Q.removeP = 0;
  1562.  
  1563. let isInvalidAdding = Q.mutationTarget && !Q.mutationTarget.parentNode
  1564.  
  1565. let promisesForAddition = !scriptEnable ? [] : addP > 0 && !isInvalidAdding ? [
  1566.  
  1567. $callOnceAsync('mtf_checkFlexy'),
  1568. $callOnceAsync('mtf_initalAttr_comments'),
  1569. $callOnceAsync('mtf_initalAttr_playlist'),
  1570. $callOnceAsync('mtf_initalAttr_chatroom'),
  1571. $callOnceAsync('mtf_initalAttr_engagement_panel'),
  1572. $callOnceAsync('mtf_advancedComments'),
  1573. $callOnceAsync('mtf_checkDescriptionLoaded'),
  1574. $callOnceAsync('mtf_fetchCommentsAvailable'),
  1575. $callOnceAsync('mtf_forceCheckLiveVideo'),
  1576.  
  1577. (async () => {
  1578. mtf_append_comments();
  1579. })(),
  1580.  
  1581. (async () => {
  1582. mtf_liveChatBtnF();
  1583. })(),
  1584.  
  1585. (async () => {
  1586. fixTabs();
  1587. mtf_AfterFixTabs();
  1588. })(),
  1589.  
  1590. (async () => {
  1591. mtf_append_playlist();
  1592. })()
  1593. ] : [];
  1594.  
  1595.  
  1596.  
  1597. let promisesForEveryMutation = !scriptEnable ? [] : [
  1598. (async () => {
  1599. mtf_fix_details();
  1600. })(),
  1601. (async () => {
  1602. mtf_ChatExist();
  1603. })()
  1604. ];
  1605.  
  1606.  
  1607. Promise.all([...promisesForAddition, ...promisesForEveryMutation]).then(() => {
  1608. Q.mutationTarget = null;
  1609.  
  1610. Q.mtoNav_requestNo--;
  1611. //console.log('motnav reduced to', mtoNav_requestNo)
  1612. if (Q.mtoNav_requestNo > 0) {
  1613. Q.mtoNav_requestNo = 1;
  1614. setTimeout(FP.mtoNav_delayedF, mtoInterval);
  1615. }
  1616. })
  1617.  
  1618.  
  1619. },
  1620.  
  1621. mtoNavF: (mutations, observer) => {
  1622.  
  1623. if (!scriptEnable) return;
  1624.  
  1625. let ch = false;
  1626.  
  1627. let reg = [];
  1628. let dTarget = null;
  1629.  
  1630. let wAddP = 0,
  1631. wRemoveP = 0;
  1632.  
  1633. let _last_mto_target = null;
  1634. let _last_mto_target_valid = null;
  1635.  
  1636. for (const mutation of mutations) {
  1637. if (!mutation || !mutation.target || !mutation.target.parentNode) continue;
  1638.  
  1639. let elementalMutation = false;
  1640. let tAddP = 0,
  1641. tRemoveP = 0;
  1642.  
  1643. for (const addedNode of mutation.addedNodes) { //theoretically faster: only reading of states
  1644. if (addedNode.nodeType === 1) {
  1645. tAddP++;
  1646. elementalMutation = true;
  1647. }
  1648. }
  1649.  
  1650. for (const removedNode of mutation.removedNodes) { //theoretically faster: only reading of states
  1651. if (removedNode.nodeType === 1) {
  1652. tRemoveP++;
  1653. elementalMutation = true;
  1654. }
  1655. }
  1656.  
  1657. if (elementalMutation) { //skip all addition and removal operations without elemental changes (e.g. textNode modification)
  1658.  
  1659. if (_last_mto_target === mutation.target) {
  1660. // due to addition and removal operations to the same DOM
  1661. if (_last_mto_target_valid) {
  1662. // AddP & RemoveP is still valid
  1663. wAddP += tAddP;
  1664. wRemoveP += tRemoveP;
  1665. }
  1666. continue;
  1667. }
  1668. _last_mto_target = mutation.target;
  1669.  
  1670. if (isMtoTargetSkip(mutation)) {
  1671. _last_mto_target_valid = false;
  1672. continue; //theoretically slower: creation of string variables
  1673. } else {
  1674. _last_mto_target_valid = true;
  1675. wAddP += tAddP;
  1676. wRemoveP += tRemoveP;
  1677. }
  1678.  
  1679.  
  1680.  
  1681. ch = true;
  1682.  
  1683. reg.push(mutation);
  1684.  
  1685. if (dTarget === null) dTarget = mutation.target; //first
  1686. else if (dTarget === true) {} //ytdFlexy
  1687. else if (dTarget.contains(mutation.target)) {} //first node is the container to all subsequential targets
  1688. else { dTarget = true; } //the target is not the child of first node
  1689.  
  1690. }
  1691.  
  1692. }
  1693.  
  1694. if (!ch) return; // dTarget must be true OR HTMLElement
  1695.  
  1696. if (dTarget === true) dTarget = kRef(ytdFlexy); // major mutation occurance
  1697. else if (dTarget === kRef(comments_section_loaded_elm) && wAddP > wRemoveP) return true; // ignore if comments are loaded (adding comments)
  1698. else if (isMtoOverallSkip(dTarget)) return; // allow for multiple mutations at the same time - determinated after looping
  1699.  
  1700.  
  1701.  
  1702. // 4 ~ 16 times per full page loading
  1703.  
  1704. Q.addP += wAddP;
  1705. Q.removeP += wRemoveP;
  1706.  
  1707. if (Q.mutationTarget === null) Q.mutationTarget = dTarget;
  1708. else if (Q.mutationTarget != dTarget) Q.mutationTarget = kRef(ytdFlexy);
  1709.  
  1710. //console.log(prettyElm(dTarget), wAddP , wRemoveP, mtoInterval)
  1711. //console.log(prettyElm(dTarget), reg.map(m=>prettyElm(m.target)))
  1712. //console.log(7015, performance.now())
  1713.  
  1714. Q.mtoNav_requestNo++;
  1715. if (Q.mtoNav_requestNo == 1) setTimeout(FP.mtoNav_delayedF, mtoInterval);
  1716.  
  1717. },
  1718.  
  1719.  
  1720. mtoBodyF: function(mutations, observer) {
  1721. if (!scriptEnable) return;
  1722.  
  1723. for (const mutation of mutations) {
  1724. for (const addedNode of mutation.addedNodes)
  1725. if (addedNode.nodeType === 1) {
  1726. if (addedNode.nodeName == "DIV" && addedNode.className.indexOf('autocomplete-suggestions') >= 0) {
  1727. mtf_fixAutoCompletePosition(addedNode)
  1728. }
  1729. }
  1730. }
  1731.  
  1732. },
  1733.  
  1734. mtf_attrPlaylist: (mutations, observer) => {
  1735.  
  1736. if (!scriptEnable) return;
  1737. let cssElm = kRef(ytdFlexy);
  1738. if (!cssElm) return;
  1739.  
  1740. var playlist = document.querySelector('ytd-playlist-panel-renderer#playlist')
  1741. const tabBtn = document.querySelector('[userscript-tab-content="#tab-list"]');
  1742. if (tabBtn) {
  1743. //console.log('attr playlist changed')
  1744. if (tabBtn.classList.contains('tab-btn-hidden') && !playlist.hasAttribute('hidden')) {
  1745. //console.log('attr playlist changed - no hide')
  1746. tabBtn.classList.remove("tab-btn-hidden");
  1747. } else if (!tabBtn.classList.contains('tab-btn-hidden') && playlist.hasAttribute('hidden')) {
  1748. //console.log('attr playlist changed - add hide')
  1749. hideTabBtn(tabBtn);
  1750. }
  1751. }
  1752. /* visible layout for triggering hidden removal */
  1753. akAttr(cssElm, 'tabview-youtube-playlist', playlist.hasAttribute('hidden'));
  1754. },
  1755. delayed_disableComments: function() {
  1756.  
  1757. let ytdFlexyElm = kRef(ytdFlexy);
  1758. if (!scriptEnable || !ytdFlexyElm) return;
  1759.  
  1760. if (status_commentsHidden() == 2) _disableComments();
  1761.  
  1762. },
  1763. mtf_attrComments: (mutations, observer) => {
  1764.  
  1765. let ytdFlexyElm = kRef(ytdFlexy);
  1766. if (!scriptEnable || !ytdFlexyElm) return;
  1767.  
  1768. var comments = document.querySelector('ytd-comments#comments')
  1769. const tabBtn = document.querySelector('[userscript-tab-content="#tab-comments"]');
  1770. if (!comments || !tabBtn) return;
  1771. let isCommentHidden = comments.hasAttribute('hidden')
  1772. //console.log('attr comments changed')
  1773.  
  1774.  
  1775. timeout_attrComments.clear();
  1776. $('span#tab3-txt-loader').text('');
  1777. mtoInterval = mtoInterval1;
  1778.  
  1779. if (!isCommentHidden) {
  1780.  
  1781. akAttr(ytdFlexyElm, 'tabview-youtube-comments', false, 'K');
  1782.  
  1783. //console.log('attr comments changed - no hide')
  1784. tabBtn.classList.remove("tab-btn-hidden") //if contains
  1785.  
  1786. makeBodyScroll();
  1787. Q.fetchedOnce = false
  1788. FOnce.mtf_fetchCommentsAvailable = FP.mtf_fetchCommentsAvailable;
  1789. if (Q.mutationTarget === null) $callOnceAsync('mtf_fetchCommentsAvailable');
  1790.  
  1791. window.requestAnimationFrame(() => {
  1792. let innerCommentsLoaderRet = innerCommentsLoader();
  1793. if (innerCommentsLoaderRet) innerCommentsLoaderRet.f();
  1794. })
  1795.  
  1796. } else if (isCommentHidden) {
  1797.  
  1798.  
  1799. //console.log('attr comments changed - add hide')
  1800.  
  1801. akAttr(ytdFlexyElm, 'tabview-youtube-comments', true, 'K');
  1802.  
  1803. let t = status_commentsHidden() == 2 ? 1630 : 760;
  1804.  
  1805. timeout_attrComments.set(FP.delayed_disableComments, t);
  1806.  
  1807. }
  1808.  
  1809. requestingComments = comments;
  1810. scrollForComments();
  1811. setTimeout(() => nativeFunc(comments, "loadComments"), 20)
  1812.  
  1813.  
  1814. },
  1815.  
  1816.  
  1817. mtf_attrChatroom: (mutations, observer) => {
  1818. let ytdFlexyElm = kRef(ytdFlexy);
  1819. if (!scriptEnable || !ytdFlexyElm) return;
  1820.  
  1821. layoutStatusMutex.lockWith(unlock => {
  1822.  
  1823. const chatBlock = document.querySelector('ytd-live-chat-frame#chat')
  1824. const cssElm = kRef(ytdFlexy)
  1825.  
  1826. if (!chatBlock || !cssElm) {
  1827. unlock();
  1828. return;
  1829. }
  1830.  
  1831. if (!cssElm.hasAttribute('userscript-chatblock')) wAttr(cssElm, 'userscript-chatblock', true);
  1832. let isCollapsed = !!chatBlock.hasAttribute('collapsed');
  1833. wAttr(cssElm, 'userscript-chat-collapsed', isCollapsed);
  1834.  
  1835. if (cssElm.hasAttribute('userscript-chatblock') && !isCollapsed) lastShowTab = '#chatroom';
  1836.  
  1837. if (!isCollapsed && document.querySelector('#right-tabs .tab-btn.active') && isWideScreenWithTwoColumns() && !isTheater()) {
  1838. switchTabActivity(null);
  1839. setTimeout(unlock, 40);
  1840. } else {
  1841. unlock();
  1842. }
  1843.  
  1844. if (!isCollapsed) {
  1845. runAfterExpandChat();
  1846. } else {
  1847. chatBlock.removeAttribute('yt-userscript-iframe-loaded');
  1848. }
  1849.  
  1850. })
  1851.  
  1852.  
  1853.  
  1854.  
  1855. },
  1856.  
  1857. mtf_attrEngagementPanel: (mutations, observer) => {
  1858. let ytdFlexyElm = kRef(ytdFlexy);
  1859. if (!scriptEnable || !ytdFlexyElm) return;
  1860.  
  1861. //multiple instance
  1862. if (mutations) {
  1863. let cPanels = null;
  1864. for (const mutation of mutations) {
  1865. let ePanel = mutation.target;
  1866. if (ePanel.getAttribute('visibility') == 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED') {
  1867. cPanels = cPanels || engagement_panels_();
  1868. for (const entry of cPanels.list) {
  1869. if (entry.ePanel != ePanel && entry.visible) ytBtnCloseEngagementPanel(entry.ePanel);
  1870. }
  1871. }
  1872. }
  1873. }
  1874.  
  1875. layoutStatusMutex.lockWith(unlock => {
  1876.  
  1877. const ePanel = document.querySelector('ytd-watch-flexy ytd-engagement-panel-section-list-renderer')
  1878. const cssElm = kRef(ytdFlexy)
  1879.  
  1880. if (!ePanel || !cssElm) {
  1881. unlock();
  1882. return;
  1883. }
  1884. let previousValue = +cssElm.getAttribute('userscript-engagement-panel') || 0;
  1885.  
  1886. let { value, count } = engagement_panels_();
  1887. let nextValue = value;
  1888. let nextCount = count;
  1889.  
  1890.  
  1891. if (nextCount == 0 && cssElm.hasAttribute('userscript-engagement-panel')) {
  1892. wAttr(cssElm, 'userscript-engagement-panel', false);
  1893. unlock();
  1894. } else {
  1895.  
  1896. if ((nextCount > 1) || (cssElm.hasAttribute('userscript-engagement-panel') && previousValue === nextValue)) {
  1897. unlock();
  1898. return;
  1899. }
  1900.  
  1901. cssElm.setAttribute('userscript-engagement-panel', nextValue);
  1902.  
  1903. let b = false;
  1904. if (previousValue != nextValue && nextValue > 0) {
  1905. lastShowTab = `#engagement-panel-${nextValue}`;
  1906. b = true;
  1907. }
  1908.  
  1909. if (b && document.querySelector('#right-tabs .tab-btn.active') && isWideScreenWithTwoColumns() && !isTheater()) {
  1910. switchTabActivity(null);
  1911. setTimeout(unlock, 40);
  1912. } else {
  1913. unlock();
  1914. }
  1915. }
  1916.  
  1917. })
  1918.  
  1919.  
  1920.  
  1921.  
  1922. },
  1923.  
  1924. mtf_initalAttr_playlist: () => {
  1925. let ytdFlexyElm = kRef(ytdFlexy);
  1926. if (!scriptEnable || !ytdFlexyElm) return true;
  1927.  
  1928. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1929.  
  1930. var playlist = rootElement.querySelector('ytd-playlist-panel-renderer#playlist')
  1931. if (!playlist) return true;
  1932. initMutationObserver(mtoVs, 'mtoVisibility_Playlist', FP.mtf_attrPlaylist).observe(playlist, {
  1933. attributes: true,
  1934. attributeFilter: ['hidden'],
  1935. attributeOldValue: true
  1936. })
  1937. FP.mtf_attrPlaylist()
  1938. return false;
  1939. },
  1940.  
  1941. mtf_initalAttr_comments: () => {
  1942. let ytdFlexyElm = kRef(ytdFlexy);
  1943. if (!scriptEnable || !ytdFlexyElm) return true;
  1944.  
  1945. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1946.  
  1947. var comments = rootElement.querySelector('ytd-comments#comments')
  1948. if (!comments) return true;
  1949. initMutationObserver(mtoVs, 'mtoVisibility_Comments', FP.mtf_attrComments).observe(comments, {
  1950. attributes: true,
  1951. attributeFilter: ['hidden'],
  1952. attributeOldValue: true
  1953. })
  1954. FP.mtf_attrComments()
  1955. requestingComments = comments;
  1956. //scrollForComments()
  1957. setTimeout(() => nativeFunc(comments, "loadComments"), 20)
  1958. return false;
  1959. },
  1960.  
  1961. mtf_initalAttr_chatroom: () => {
  1962. let ytdFlexyElm = kRef(ytdFlexy);
  1963. if (!scriptEnable || !ytdFlexyElm) return true;
  1964.  
  1965. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1966.  
  1967. var chatroom = rootElement.querySelector('ytd-live-chat-frame#chat')
  1968. if (!chatroom) return true;
  1969. initMutationObserver(mtoVs, 'mtoVisibility_Chatroom', FP.mtf_attrChatroom).observe(chatroom, {
  1970. attributes: true,
  1971. attributeFilter: ['collapsed'],
  1972. attributeOldValue: true
  1973. })
  1974. FP.mtf_attrChatroom()
  1975. return false;
  1976. },
  1977.  
  1978. mtf_initalAttr_engagement_panel: () => {
  1979. let ytdFlexyElm = kRef(ytdFlexy);
  1980. if (!scriptEnable || !ytdFlexyElm) return true;
  1981.  
  1982. const rootElement = Q.mutationTarget || ytdFlexyElm;
  1983.  
  1984. let toCheck = false;
  1985. for (const engagement_panel of rootElement.querySelectorAll('ytd-watch-flexy ytd-engagement-panel-section-list-renderer:not([tabview-attr-checked])')) {
  1986.  
  1987. engagement_panel.setAttribute('tabview-attr-checked', '');
  1988. let mto = mtoVs.mtoVisibility_EngagementPanel;
  1989. if (!mto) mto = initMutationObserver(mtoVs, 'mtoVisibility_EngagementPanel', FP.mtf_attrEngagementPanel);
  1990. mto.observe(engagement_panel, {
  1991. attributes: true,
  1992. attributeFilter: ['visibility'],
  1993. attributeOldValue: true
  1994. })
  1995. toCheck = true;
  1996. }
  1997.  
  1998. if (toCheck) FP.mtf_attrEngagementPanel()
  1999.  
  2000. return true;
  2001. },
  2002.  
  2003. mtf_checkDescriptionLoaded: () => {
  2004.  
  2005. let ytdFlexyElm = kRef(ytdFlexy);
  2006. if (!scriptEnable || !ytdFlexyElm) return true;
  2007.  
  2008. const rootElement = Q.mutationTarget || ytdFlexyElm;
  2009.  
  2010. const expander = document.querySelector("#meta-contents ytd-expander");
  2011. if (!expander) return true;
  2012. $(expander).appendTo("#tab-info").attr('data-dom-changed-by-tabview-youtube', scriptVersionForExternal)
  2013.  
  2014. const avatar = document.querySelector('#meta-contents yt-img-shadow#avatar');
  2015. if (avatar) hackImgShadow(avatar)
  2016. return false;
  2017.  
  2018. },
  2019.  
  2020.  
  2021. //live-chat / chat-replay
  2022.  
  2023. fireOnce_forceCheckLiveVideo_tf: () => {
  2024.  
  2025. let ytdFlexyElm = kRef(ytdFlexy);
  2026. if (!scriptEnable || !ytdFlexyElm) return;
  2027.  
  2028. let elm = document.querySelector('#ytd-player .ytp-time-display');
  2029. if (elm && elm.classList.contains('ytp-live')) {
  2030. //console.log(7006)
  2031. ytdFlexyElm.setAttribute('userscript-chatblock', 'chat-live')
  2032. //disableComments_LiveChat();
  2033. }
  2034. },
  2035.  
  2036. mtf_forceCheckLiveVideo: () => {
  2037. let ytdFlexyElm = kRef(ytdFlexy);
  2038. if (!scriptEnable || !ytdFlexyElm) return true;
  2039.  
  2040. const rootElement = Q.mutationTarget || ytdFlexyElm;
  2041. const playerLabel = document.querySelector('#ytd-player .ytp-time-display') && document.querySelector('ytd-live-chat-frame#chat')
  2042. if (!playerLabel) return true;
  2043. setTimeout(FP.fireOnce_forceCheckLiveVideo_tf, 170)
  2044. return false;
  2045. },
  2046.  
  2047. //comments
  2048.  
  2049. mtf_advancedComments: () => {
  2050.  
  2051. let ytdFlexyElm = kRef(ytdFlexy);
  2052. if (!scriptEnable || !ytdFlexyElm) return true;
  2053.  
  2054. const rootElement = Q.mutationTarget || ytdFlexyElm;
  2055. const continuations = document.querySelector("ytd-comments#comments #continuations");
  2056. if (!continuations) return true;
  2057. requestingComments = document.querySelector('ytd-comments#comments');
  2058. scrollForComments();
  2059. return false;
  2060. },
  2061.  
  2062. mtf_fetchCommentsAvailable: () => {
  2063.  
  2064. let ytdFlexyElm = kRef(ytdFlexy);
  2065. if (!scriptEnable || !ytdFlexyElm) return true;
  2066.  
  2067. let innerCommentsLoaderRet = innerCommentsLoader();
  2068. if (!innerCommentsLoaderRet) return true;
  2069.  
  2070. innerCommentsLoaderRet.f();
  2071. fetchCommentsFinished();
  2072. return false;
  2073.  
  2074. }
  2075.  
  2076.  
  2077. }
  2078.  
  2079.  
  2080.  
  2081.  
  2082. function initObserver() {
  2083.  
  2084. Q.addP = 0;
  2085. Q.removeP = 0;
  2086. Q.mutationTarget = null;
  2087.  
  2088. Q.mtoNav_requestNo = 0;
  2089.  
  2090. initMutationObserver(mtoCs, 'mtoNav', FP.mtoNavF).observe(kRef(ytdFlexy), {
  2091. subtree: true,
  2092. childList: true,
  2093. attributes: false
  2094. });
  2095.  
  2096. -
  2097. (async () => {
  2098. Q.addP = 1; //force all checking
  2099. Q.mtoNav_requestNo++;
  2100. if (Q.mtoNav_requestNo == 1) FP.mtoNav_delayedF();
  2101. })();
  2102.  
  2103. // for automcomplete plugin
  2104. initMutationObserver(mtoCs, 'mtoBody', FP.mtoBodyF).observe(document.querySelector('body'), {
  2105. childList: true,
  2106. subtree: false,
  2107. attributes: false
  2108. })
  2109.  
  2110.  
  2111. }
  2112.  
  2113. let displayedPlaylist = null;
  2114. let scrollingVideosList = null;
  2115.  
  2116. let scriptEnable = false;
  2117. let scriptEC = 0;
  2118. let lastShowTab = null;
  2119.  
  2120. let _cachedLastVideo = null;
  2121. let videoListBeforeSearch = null;
  2122. let no_fix_contents_until = 0;
  2123. let no_fix_playlist_until = 0;
  2124. let statusCollasped = 0;
  2125.  
  2126. let ytdFlexy = null;
  2127. let timeout_attrComments = new Timeout();
  2128.  
  2129. function pluginHook() {
  2130.  
  2131. scriptEnable = true;
  2132. scriptEC++;
  2133.  
  2134. no_fix_contents_until = 0;
  2135. no_fix_playlist_until = 0;
  2136.  
  2137. ytdFlexy = mWeakRef(document.querySelector('ytd-watch-flexy'))
  2138.  
  2139. }
  2140.  
  2141. function pluginUnhook() {
  2142.  
  2143. //console.log(8001)
  2144. timeout_attrComments.clear();
  2145.  
  2146. videoListBeforeSearch = null;
  2147. statusCollasped = 0;
  2148. _cachedLastVideo = null;
  2149. lastShowTab = null;
  2150. displayedPlaylist = null;
  2151. scrollingVideosList = null;
  2152. scriptEnable = false;
  2153. scriptEC++;
  2154. if (scriptEC > 788888888) scriptEC = 188888888;
  2155. ytdFlexy = null;
  2156. wls.layoutStatus = null;
  2157.  
  2158. clearMutationObserver(mtoVs, 'mtoVisibility_Playlist')
  2159. clearMutationObserver(mtoVs, 'mtoVisibility_Comments')
  2160. clearMutationObserver(mtoVs, 'mtoVisibility_Chatroom')
  2161. clearMutationObserver(mtoVs, 'mtoFlexyAttr')
  2162.  
  2163. clearMutationObserver(mtoCs, 'mtoBody')
  2164. if (clearMutationObserver(mtoCs, 'mtoNav')) {
  2165. FOnce.mtf_checkFlexy = null;
  2166. FOnce.mtf_initalAttr_comments = null;
  2167. FOnce.mtf_initalAttr_playlist = null;
  2168. FOnce.mtf_initalAttr_chatroom = null;
  2169. FOnce.mtf_initalAttr_engagement_panel = null;
  2170. FOnce.mtf_advancedComments = null;
  2171. FOnce.mtf_checkDescriptionLoaded = null;
  2172. FOnce.mtf_fetchCommentsAvailable = null;
  2173. FOnce.mtf_forceCheckLiveVideo = null;
  2174. Q.mtf_chatBlockQ = null;
  2175. }
  2176.  
  2177. mtoInterval = mtoInterval1;
  2178.  
  2179. }
  2180.  
  2181. let comments_section_loaded_elm = null;
  2182.  
  2183.  
  2184. function initializeForVideoChange() {
  2185.  
  2186.  
  2187.  
  2188. pluginUnhook();
  2189. if (!document.querySelector('script#userscript-tabview-injection-1')) {
  2190.  
  2191. if (isMyScriptInChromeRuntime())
  2192. addScriptByURL(window.chrome.runtime.getURL('js/injection_script_1.js')).id = 'userscript-tabview-injection-1';
  2193. else
  2194. addScript(`!!(${injection_script_1+""})()`).id = 'userscript-tabview-injection-1'
  2195. // addScriptByURL(`${githubURLBase}/${githubURLCommit}/js/injection_script_1.js`).id='userscript-tabview-injection-1';
  2196.  
  2197. }
  2198. var prevComemnts = document.querySelector('ytd-comments#comments');
  2199.  
  2200. if (prevComemnts && prevComemnts.matches('[hidden]')) {
  2201.  
  2202. // assumption: loading comments after executation of this function which removes the attribute [hidden] of #ytd-comments#comments
  2203.  
  2204. var prevCommentsHeader = prevComemnts.querySelector('ytd-comments#comments ytd-comments-header-renderer');
  2205. var prevCommentsMsg = prevComemnts.querySelector('ytd-item-section-renderer#sections #header ~ #contents>ytd-message-renderer');
  2206.  
  2207. //removed any cache of #comments header (i.e. count message)
  2208. if (prevCommentsHeader) prevCommentsHeader.parentNode.removeChild(prevCommentsHeader);
  2209.  
  2210. //removed any cache of #comments message (i.e. 留言功能已停用。)
  2211. if (prevCommentsMsg) prevCommentsMsg.parentNode.removeChild(prevCommentsMsg);
  2212.  
  2213.  
  2214.  
  2215. }
  2216.  
  2217.  
  2218. window.requestAnimationFrame(() => {
  2219.  
  2220. // requestAnimationFrame is required to let youtube coding running first !
  2221.  
  2222. pluginHook();
  2223.  
  2224. _onNavigationEnd();
  2225.  
  2226. setTimeout(() => {
  2227.  
  2228. //ensure comment tab text is updated - no matter there is change of video or not
  2229. let innerCommentsLoaderRet = innerCommentsLoader();
  2230. if (innerCommentsLoaderRet) innerCommentsLoaderRet.f();
  2231.  
  2232. }, 40);
  2233.  
  2234.  
  2235. })
  2236.  
  2237.  
  2238. return true;
  2239.  
  2240. }
  2241.  
  2242. function getTabsHTML() {
  2243.  
  2244. const sTabBtnVideos = `${svgElm(16,16,298,298,svgVideos)}<span>Videos</span>`
  2245. const sTabBtnInfo = `${svgElm(16,16,23.625,23.625,svgInfo)}<span>Info</span>`
  2246. const sTabBtnPlayList = `${svgElm(16,16,426.667,426.667,svgPlayList)}<span>Playlist</span>`
  2247.  
  2248. let str1 = `
  2249. <paper-ripple class="style-scope yt-icon-button">
  2250. <div id="background" class="style-scope paper-ripple" style="opacity:0;"></div>
  2251. <div id="waves" class="style-scope paper-ripple"></div>
  2252. </paper-ripple>
  2253. `;
  2254.  
  2255. const str_tabs = [
  2256. `<a id="tab-btn1" data-name="info" userscript-tab-content="#tab-info" class="tab-btn">${sTabBtnInfo}${str1}</a>`,
  2257. `<a id="tab-btn2" userscript-tab-content="#tab-live" class="tab-btn tab-btn-hidden">Chat${str1}</a>`,
  2258. `<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>`,
  2259. `<a id="tab-btn4" userscript-tab-content="#tab-videos" data-name="videos" class="tab-btn">${sTabBtnVideos}${str1}</a>`,
  2260. `<a id="tab-btn5" userscript-tab-content="#tab-list" class="tab-btn">${sTabBtnPlayList}${str1}</a>`
  2261. ].join('')
  2262.  
  2263. var addHTML = `
  2264. <div id="right-tabs">
  2265. <header>
  2266. <div id="material-tabs">
  2267. ${str_tabs}
  2268. </div>
  2269. </header>
  2270. <div class="tab-content">
  2271. <div id="tab-info" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2272. <div id="tab-live" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2273. <div id="tab-comments" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2274. <div id="tab-videos" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2275. <div id="tab-list" class="tab-content-cld tab-content-hidden" userscript-scrollbar-render></div>
  2276. </div>
  2277. </div>
  2278. `;
  2279.  
  2280. return addHTML
  2281.  
  2282. }
  2283.  
  2284.  
  2285. function injection_script_1() {
  2286.  
  2287. document.addEventListener('userscript-call-dom-func', function(evt) {
  2288.  
  2289. if (!evt || !evt.target || !evt.detail) return;
  2290. let dom = evt.target;
  2291.  
  2292. let { property, args } = evt.detail;
  2293. if (!property) return;
  2294. let f = dom[property];
  2295. if (typeof f != 'function') return;
  2296.  
  2297. if (args && args.length > 0) f.apply(dom, args);
  2298. else f.call(dom);
  2299.  
  2300. }, true)
  2301.  
  2302. }
  2303.  
  2304. function _onNavigationEnd() {
  2305.  
  2306. let timeoutR_findRelated = new Timeout();
  2307. timeoutR_findRelated.set(function() {
  2308. let related = kRef(ytdFlexy).querySelector("#related");
  2309. if (!related) return true;
  2310. foundRelated(related);
  2311. }, 100, 10)
  2312.  
  2313. function foundRelated(related) {
  2314. let promise = Promise.resolve();
  2315. if (!document.querySelector("#right-tabs")) {
  2316. promise = promise.then(() => {
  2317. $(getTabsHTML()).insertBefore(related).attr('data-dom-created-by-tabview-youtube', scriptVersionForExternal);
  2318. })
  2319. }
  2320. promise.then(runAfterTabAppended).then(initObserver)
  2321. }
  2322.  
  2323. setTimeout(() => {
  2324. for (const s of document.querySelectorAll('#right-tabs [userscript-scrollbar-render]')) {
  2325. Promise.resolve(s).then(s => {
  2326. if (s && s.scrollTop > 0) s.scrollTop = 0;
  2327. let child = s.firstElementChild;
  2328. if (child && child.scrollTop > 0) child.scrollTop = 0;
  2329. });
  2330. }
  2331. }, 90)
  2332.  
  2333. }
  2334.  
  2335. function onNavigationEnd(evt) {
  2336.  
  2337. pluginUnhook(); // in case not triggered by popstate - say mini playing
  2338.  
  2339. $('span#tab3-txt-loader').text('');
  2340. setCommentSection(0)
  2341.  
  2342. if (!/^https?\:\/\/(\w+\.)*youtube\.com\/watch\?(\w+\=[^\/\?\&]+\&|\w+\=?\&)*v=[\w\-\_]+/.test(window.location.href)) return;
  2343.  
  2344.  
  2345. let timeout = 4; // max. 4 animation frames
  2346.  
  2347. let tf = () => {
  2348. if (--timeout > 0 && !document.querySelector('#player video')) return requestAnimationFrame(tf);
  2349.  
  2350. initializeForVideoChange();
  2351. pluginHook();
  2352. _onNavigationEnd();
  2353.  
  2354. }
  2355.  
  2356.  
  2357. tf();
  2358.  
  2359.  
  2360.  
  2361.  
  2362. }
  2363.  
  2364. function setToActiveTab(defaultTab) {
  2365. if (isTheater() && isWideScreenWithTwoColumns()) return;
  2366. const jElm = document.querySelector(`a[userscript-tab-content="${switchTabActivity_lastTab}"]:not(.tab-btn-hidden)`) ||
  2367. document.querySelector(`a[userscript-tab-content="${(defaultTab||settings.defaultTab)}"]:not(.tab-btn-hidden)`) ||
  2368. document.querySelector("a[userscript-tab-content]:not(.tab-btn-hidden)") ||
  2369. null;
  2370. switchTabActivity(jElm);
  2371. return !!jElm;
  2372. }
  2373.  
  2374. function getWrapper(wrapperId) {
  2375. let $wrapper = $(`#${wrapperId}`);
  2376. if (!$wrapper[0]) $wrapper = $(`<div id="${wrapperId}"></div>`)
  2377. return $wrapper.first();
  2378. }
  2379.  
  2380. function runAfterTabAppended() {
  2381.  
  2382. document.documentElement.setAttribute('plugin-tabview-youtube', '')
  2383.  
  2384. const cssElm = kRef(ytdFlexy)
  2385. if (cssElm && !cssElm.hasAttribute('tabview-selection')) cssElm.setAttribute('tabview-selection', '')
  2386.  
  2387.  
  2388. // append the next videos
  2389. // it exists as "related" is already here
  2390. fixTabs();
  2391.  
  2392. // just switch to the default tab
  2393. setToActiveTab();
  2394.  
  2395.  
  2396. prepareTabBtn();
  2397.  
  2398.  
  2399. // append the detailed meta contents to the tab-info
  2400. FOnce.mtf_checkDescriptionLoaded = FP.mtf_checkDescriptionLoaded;
  2401. if (Q.mutationTarget === null) $callOnceAsync('mtf_checkDescriptionLoaded');
  2402.  
  2403. // force window scroll when #continuations is first detected and #comments still [hidden]
  2404. FOnce.mtf_advancedComments = FP.mtf_advancedComments;
  2405. if (Q.mutationTarget === null) $callOnceAsync('mtf_advancedComments');
  2406.  
  2407. // use video player's element to detect the live-chat situation (no commenting section)
  2408. // 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
  2409. FOnce.mtf_forceCheckLiveVideo = FP.mtf_forceCheckLiveVideo;
  2410. if (Q.mutationTarget === null) $callOnceAsync('mtf_forceCheckLiveVideo');
  2411.  
  2412.  
  2413. // Attr Mutation Observer - #playlist - hidden
  2414. clearMutationObserver(mtoVs, 'mtoVisibility_Playlist')
  2415. // Attr Mutation Observer callback - #playlist - hidden
  2416.  
  2417. // pending for #playlist and set Attribute Observer
  2418. FOnce.mtf_initalAttr_playlist = FP.mtf_initalAttr_playlist
  2419. if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_playlist');
  2420.  
  2421. // Attr Mutation Observer - ytd-comments#comments - hidden
  2422. clearMutationObserver(mtoVs, 'mtoVisibility_Comments')
  2423. // Attr Mutation Observer callback - ytd-comments#comments - hidden
  2424.  
  2425. // pending for #comments and set Attribute Observer
  2426. FOnce.mtf_initalAttr_comments = FP.mtf_initalAttr_comments;
  2427. if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_comments');
  2428.  
  2429.  
  2430. clearMutationObserver(mtoVs, 'mtoVisibility_Chatroom');
  2431. FOnce.mtf_initalAttr_chatroom = FP.mtf_initalAttr_chatroom
  2432. if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_chatroom');
  2433.  
  2434. clearMutationObserver(mtoVs, 'mtoVisibility_EngagementPanel');
  2435. for (const engagement_panel of document.querySelectorAll('ytd-watch-flexy ytd-engagement-panel-section-list-renderer:not([tabview-attr-checked])')) {
  2436. engagement_panel.removeAttribute('tabview-attr-checked');
  2437. }
  2438. FOnce.mtf_initalAttr_engagement_panel = FP.mtf_initalAttr_engagement_panel
  2439. if (Q.mutationTarget === null) $callOnceAsync('mtf_initalAttr_engagement_panel');
  2440.  
  2441.  
  2442. flexyAttrInit();
  2443.  
  2444. document.querySelector("#right-tabs .tab-content").addEventListener('scroll', makeBodyScrollByEvt, true);
  2445.  
  2446. }
  2447.  
  2448.  
  2449. function fetchCommentsFinished() {
  2450. let ytdFlexyElm = kRef(ytdFlexy);
  2451. if (!scriptEnable || !ytdFlexyElm) return;
  2452. timeout_attrComments.clear();
  2453. akAttr(ytdFlexyElm, 'tabview-youtube-comments', false, 'LS')
  2454. }
  2455.  
  2456. function setCommentSection(value) {
  2457. Q.comments_section_loaded = value;
  2458.  
  2459. if (value === 0) {
  2460. comments_section_loaded_elm = null;
  2461. }
  2462.  
  2463. }
  2464.  
  2465. function _disableComments() {
  2466.  
  2467. if (!scriptEnable) return;
  2468. let cssElm = kRef(ytdFlexy);
  2469. if (!cssElm) return;
  2470.  
  2471. mtoInterval = mtoInterval2;
  2472. setCommentSection(2);
  2473.  
  2474. comments_section_loaded_elm = mWeakRef(document.querySelector('ytd-comments#comments div#contents') || null)
  2475.  
  2476. let tabBtn = document.querySelector('.tab-btn[userscript-tab-content="#tab-comments"]');
  2477. if (tabBtn && !tabBtn.classList.contains('tab-btn-hidden')) {
  2478. hideTabBtn(tabBtn)
  2479. }
  2480.  
  2481. akAttr(cssElm, 'tabview-youtube-comments', true, 'D');
  2482. FOnce.mtf_fetchCommentsAvailable = null;
  2483.  
  2484.  
  2485. }
  2486.  
  2487.  
  2488.  
  2489.  
  2490. let layoutStatusMutex = new Mutex();
  2491.  
  2492. function forceDisplayChatReplay() {
  2493. let items = chatFrameElement('yt-live-chat-item-list-renderer #items');
  2494. if (items && items.childElementCount !== 0) return;
  2495.  
  2496. let ytd_player = document.querySelector('ytd-player#ytd-player');
  2497. if (!ytd_player) return;
  2498. let videoElm = ytd_player.querySelector('video');
  2499. if (!videoElm) return;
  2500.  
  2501. let video = videoElm;
  2502. if (videoElm && video.currentTime > 0 && !video.ended && video.readyState > video.HAVE_CURRENT_DATA) {
  2503. let chat = document.querySelector('ytd-live-chat-frame#chat');
  2504. if (chat) {
  2505. nativeFunc(chat, "postToContentWindow", [{ "yt-player-video-progress": videoElm.currentTime }])
  2506. }
  2507. }
  2508.  
  2509. }
  2510.  
  2511.  
  2512.  
  2513. function runAfterExpandChat() {
  2514.  
  2515.  
  2516.  
  2517. new Promise(resolve => {
  2518.  
  2519.  
  2520. let chatFrame_st = Date.now();
  2521. let cid_chatFrameCheck = 0;
  2522.  
  2523. let sEF = new ScriptEF();
  2524. cid_chatFrameCheck = setInterval(() => {
  2525. if (!sEF.isValid()) return cid_chatFrameCheck = clearInterval(cid_chatFrameCheck);
  2526. let cDoc = chatFrameContentDocument();
  2527. if (cDoc) {
  2528. cid_chatFrameCheck = clearInterval(cid_chatFrameCheck);
  2529. resolve();
  2530. } else if (!scriptEnable || !isChatExpand() || Date.now() - chatFrame_st > 6750) {
  2531. cid_chatFrameCheck = clearInterval(cid_chatFrameCheck);
  2532. }
  2533. }, 60);
  2534.  
  2535.  
  2536. }).then(() => new Promise(resolve => {
  2537.  
  2538.  
  2539.  
  2540.  
  2541. let timeoutR_ChatAppReady = new Timeout();
  2542. timeoutR_ChatAppReady.set(() => {
  2543.  
  2544. if (!scriptEnable || !isChatExpand()) return false;
  2545. let app = chatFrameElement('yt-live-chat-app');
  2546. if (!app) return true;
  2547.  
  2548.  
  2549. setTimeout(() => resolve(app), 40)
  2550.  
  2551.  
  2552. }, 40, 150); //40*150 = 6000ms = 6s;
  2553.  
  2554.  
  2555.  
  2556.  
  2557. })).then(app => {
  2558.  
  2559.  
  2560. let cDoc = app.ownerDocument;
  2561.  
  2562. if (!scriptEnable || !isChatExpand()) return;
  2563. addStyle(`
  2564. body #input-panel.yt-live-chat-renderer::after {
  2565. background: transparent;
  2566. }
  2567. #items.yt-live-chat-item-list-renderer{
  2568. contain: content;
  2569. }
  2570. yt-live-chat-text-message-renderer{
  2571. contain: content;
  2572. }
  2573. #item-offset.yt-live-chat-item-list-renderer{
  2574. contain: content;
  2575. }
  2576. #item-scroller.yt-live-chat-item-list-renderer{
  2577. contain: strict;
  2578. }
  2579. img[width][height]{
  2580. contain: strict;
  2581. }
  2582. #item-list>yt-live-chat-item-list-renderer, #item-list>yt-live-chat-item-list-renderer>#contents{
  2583. contain: strict;
  2584. }
  2585. yt-live-chat-app{
  2586. contain: content;
  2587. }
  2588. `, cDoc.documentElement)
  2589.  
  2590. if (cDoc.querySelector('yt-live-chat-renderer #continuations')) {
  2591. setTimeout(() => mtf_ChatExist(), 40);
  2592. $(document.querySelector('ytd-live-chat-frame#chat')).attr('yt-userscript-iframe-loaded', '')
  2593. }
  2594.  
  2595. forceDisplayChatReplay();
  2596.  
  2597.  
  2598.  
  2599.  
  2600. })
  2601.  
  2602. }
  2603.  
  2604.  
  2605. function flexyAttrInit() {
  2606.  
  2607.  
  2608. clearMutationObserver(mtoVs, 'mtoFlexyAttr')
  2609.  
  2610. function toggleFlag(mFlag, b, flag) {
  2611. if (b) mFlag = mFlag | flag;
  2612. else mFlag = mFlag & ~flag;
  2613. return mFlag;
  2614. }
  2615.  
  2616. function toLayoutStatus(nls, attributeName) {
  2617.  
  2618. let attrElm, b, v;
  2619. switch (attributeName) {
  2620. case 'theater':
  2621. nls = toggleFlag(nls, isTheater(), LAYOUT_THEATER);
  2622. break;
  2623. case 'userscript-chat-collapsed':
  2624. case 'userscript-chatblock':
  2625. attrElm = kRef(ytdFlexy);
  2626. if (hasAttribute(attrElm, 'userscript-chat-collapsed')) {
  2627. nls = toggleFlag(nls, true, LAYOUT_CHATROOM | LAYOUT_CHATROOM_COLLASPED);
  2628. } else {
  2629. nls = toggleFlag(nls, hasAttribute(attrElm, 'userscript-chatblock'), LAYOUT_CHATROOM);
  2630. nls = toggleFlag(nls, false, LAYOUT_CHATROOM_COLLASPED);
  2631. }
  2632. break;
  2633. case 'is-two-columns_':
  2634. nls = toggleFlag(nls, isWideScreenWithTwoColumns(), LAYOUT_TWO_COLUMNS);
  2635. break;
  2636.  
  2637. case 'tabview-selection':
  2638. b = isNonEmptyString(kRef(ytdFlexy).getAttribute('tabview-selection'));
  2639. nls = toggleFlag(nls, b, LAYOUT_TAB_EXPANDED);
  2640. break;
  2641.  
  2642. case 'fullscreen':
  2643. b = isNonEmptyString(kRef(ytdFlexy).getAttribute('fullscreen'));
  2644. nls = toggleFlag(nls, b, LAYOUT_FULLSCREEN);
  2645. break;
  2646.  
  2647. case 'userscript-engagement-panel':
  2648. v = kRef(ytdFlexy).getAttribute('userscript-engagement-panel');
  2649. b = (+v > 0)
  2650. nls = toggleFlag(nls, b, LAYOUT_ENGAGEMENT_PANEL_EXPAND);
  2651. break;
  2652.  
  2653. }
  2654.  
  2655. return nls;
  2656.  
  2657.  
  2658. }
  2659.  
  2660. let mtf_attrFlexy = (mutations, observer) => {
  2661.  
  2662. if (!scriptEnable) return;
  2663.  
  2664.  
  2665. const cssElm = kRef(ytdFlexy)
  2666. if (!cssElm) return;
  2667.  
  2668. if (!mutations) return;
  2669.  
  2670. const old_layoutStatus = wls.layoutStatus
  2671. if (old_layoutStatus === null) return;
  2672. let new_layoutStatus = old_layoutStatus;
  2673.  
  2674. for (const mutation of mutations) {
  2675. new_layoutStatus = toLayoutStatus(new_layoutStatus, mutation.attributeName);
  2676. if (mutation.attributeName == 'userscript-chat-collapsed' || mutation.attributeName == 'userscript-chatblock') {
  2677.  
  2678. if (cssElm.getAttribute('userscript-chatblock') === 'chat-live') {
  2679.  
  2680. requestingComments = null;
  2681. _disableComments();
  2682.  
  2683. }
  2684.  
  2685. if (!cssElm.hasAttribute('userscript-chatblock')) {
  2686. setTimeout(() => {
  2687. if (!scriptEnable) return;
  2688. if (!isAnyActiveTab() && !isChatExpand() && !isTheater() && isWideScreenWithTwoColumns() && !isFullScreen()) {
  2689. setToActiveTab();
  2690. }
  2691. }, 240);
  2692. }
  2693.  
  2694. }
  2695. }
  2696.  
  2697.  
  2698. if (new_layoutStatus !== old_layoutStatus) wls.layoutStatus = new_layoutStatus
  2699.  
  2700.  
  2701.  
  2702.  
  2703. }
  2704.  
  2705.  
  2706. FOnce.mtf_checkFlexy = () => {
  2707. let ytdFlexyElm = kRef(ytdFlexy);
  2708. if (!scriptEnable || !ytdFlexyElm) return true;
  2709.  
  2710. const rootElement = Q.mutationTarget || ytdFlexyElm;
  2711.  
  2712. var flexy = kRef(ytdFlexy)
  2713. if (!flexy) return true;
  2714.  
  2715. wls.layoutStatus = null;
  2716.  
  2717. let rChatExist = base_ChatExist();
  2718. if (rChatExist) {
  2719. let { attr_chatblock, attr_chatcollapsed } = rChatExist;
  2720. if (attr_chatblock === null) {
  2721. //remove attribute if it is unknown
  2722. attr_chatblock = false;
  2723. attr_chatcollapsed = false;
  2724. }
  2725. wAttr(flexy, 'userscript-chatblock', attr_chatblock)
  2726. wAttr(flexy, 'userscript-chat-collapsed', attr_chatcollapsed)
  2727. }
  2728.  
  2729. let rTabSelection = [...flexy.querySelectorAll('.tab-btn[userscript-tab-content]')]
  2730. .map(elm => ({ elm, hidden: elm.classList.contains('tab-btn-hidden') }))
  2731. .filter(entry => entry.hidden === true);
  2732. if (rTabSelection.length === 0) wAttr(flexy, 'tabview-selection', false);
  2733. else if (rTabSelection.length === 1) wAttr(flexy, 'tabview-selection', rTabSelection[0].elm.getAttribute('userscript-tab-content') || '');
  2734.  
  2735. let rEP = engagement_panels_();
  2736. if (rEP && rEP.count > 0) wAttr(flexy, 'userscript-engagement-panel', false);
  2737. else wAttr(flexy, 'userscript-engagement-panel', rEP.value + "");
  2738.  
  2739. let ls = 0;
  2740. ls = toLayoutStatus(ls, 'theater')
  2741. ls = toLayoutStatus(ls, 'userscript-chat-collapsed')
  2742. ls = toLayoutStatus(ls, 'userscript-chatblock')
  2743. ls = toLayoutStatus(ls, 'is-two-columns_')
  2744. ls = toLayoutStatus(ls, 'tabview-selection')
  2745. ls = toLayoutStatus(ls, 'fullscreen')
  2746. ls = toLayoutStatus(ls, 'userscript-engagement-panel')
  2747.  
  2748. wls.layoutStatus = ls
  2749.  
  2750. initMutationObserver(mtoVs, 'mtoFlexyAttr', mtf_attrFlexy).observe(flexy, {
  2751. attributes: true,
  2752. attributeFilter: ['userscript-chat-collapsed', 'userscript-chatblock', 'theater', 'is-two-columns_', 'tabview-selection', 'fullscreen', 'userscript-engagement-panel'],
  2753. attributeOldValue: true
  2754. })
  2755.  
  2756.  
  2757. let columns = document.querySelector('ytd-page-manager#page-manager #columns')
  2758. if (columns) {
  2759. wAttr(columns, 'userscript-scrollbar-render', true);
  2760. }
  2761.  
  2762. return false;
  2763. }
  2764. if (Q.mutationTarget === null) $callOnceAsync('mtf_checkFlexy')
  2765.  
  2766.  
  2767.  
  2768. }
  2769.  
  2770.  
  2771.  
  2772.  
  2773. let switchTabActivity_lastTab = null
  2774.  
  2775. function setDisplayedPlaylist() {
  2776. //override the default youtube coding event prevention
  2777. let cssElm = kRef(ytdFlexy);
  2778. if (!scriptEnable || !cssElm) return;
  2779. displayedPlaylist = mWeakRef(document.querySelector('ytd-watch-flexy #tab-list:not(.tab-content-hidden) ytd-playlist-panel-renderer') || null);
  2780. }
  2781.  
  2782. function fixLineClampFn1(){
  2783. setTimeout(()=>requestAnimationFrame(()=>new Promise(fixLineClampFn2)),1)
  2784. }
  2785. function fixLineClampFn2(){
  2786. let contentElements = document.querySelectorAll('ytd-comments#comments ytd-expander[should-use-number-of-lines] #content')
  2787. for(const elm of contentElements) elm.classList.toggle('tabview-fix-line-clamp');
  2788. contentElements = null;
  2789. }
  2790.  
  2791. function switchTabActivity(activeLink) {
  2792. if (!scriptEnable) return;
  2793.  
  2794. const ytdFlexyElm = kRef(ytdFlexy);
  2795.  
  2796. if (!ytdFlexyElm) return;
  2797.  
  2798. if (activeLink && activeLink.classList.contains('tab-btn-hidden')) return; // not allow to switch to hide tab
  2799.  
  2800. if (isTheater() && isWideScreenWithTwoColumns()) activeLink = null;
  2801.  
  2802.  
  2803. function runAtEnd() {
  2804.  
  2805. if (activeLink) lastShowTab = activeLink.getAttribute('userscript-tab-content')
  2806.  
  2807. displayedPlaylist = null;
  2808. scrollingVideosList = null;
  2809.  
  2810. if (activeLink && lastShowTab == '#tab-list') {
  2811. setDisplayedPlaylist();
  2812. } else if (activeLink && lastShowTab == '#tab-videos') {
  2813. scrollingVideosList = mWeakRef(document.querySelector('ytd-watch-flexy #tab-videos:not(.tab-content-hidden) [placeholder-videos]'));
  2814. }
  2815.  
  2816.  
  2817. ytdFlexyElm.setAttribute('tabview-selection', activeLink ? lastShowTab : '')
  2818.  
  2819. if (lastShowTab == '#tab-comments' && (ytdFlexyElm.getAttribute('tabview-youtube-comments') || '').lastIndexOf('S') >= 0) {
  2820.  
  2821. akAttr(ytdFlexyElm, 'tabview-youtube-comments', false, 'L');
  2822.  
  2823. requestAnimationFrame(() => {
  2824. let comments_tab = document.querySelector('#tab-comments');
  2825. if (comments_tab && comments_tab.scrollTop > 0) comments_tab.scrollTop = 0;
  2826. });
  2827.  
  2828. }
  2829. if(lastShowTab=='#tab-comments'){
  2830. fixLineClampFn1();
  2831. }
  2832.  
  2833. }
  2834.  
  2835. const links = document.querySelectorAll('#material-tabs a[userscript-tab-content]');
  2836.  
  2837.  
  2838. for (const link of links) {
  2839. let content = document.querySelector(link.getAttribute('userscript-tab-content'));
  2840. if (link && content) {
  2841. if (link !== activeLink) {
  2842. link.classList.remove("active");
  2843. content.classList.add("tab-content-hidden");
  2844. } else {
  2845. link.classList.add("active");
  2846. content.classList.remove("tab-content-hidden");
  2847. //setTimeout(()=>content.focus(),400);
  2848.  
  2849. }
  2850. }
  2851. }
  2852.  
  2853. runAtEnd();
  2854.  
  2855.  
  2856. }
  2857.  
  2858. let tabsUiScript_setclick = false;
  2859.  
  2860. function prepareTabBtn() {
  2861.  
  2862. const materialTab = document.querySelector("#material-tabs")
  2863. if (!materialTab) return;
  2864.  
  2865. let noActiveTab = !!document.querySelector('ytd-watch-flexy[userscript-chatblock]:not([userscript-chat-collapsed])')
  2866.  
  2867. const activeLink = materialTab.querySelector('a[userscript-tab-content].active:not(.tab-btn-hidden)')
  2868. if (activeLink) switchTabActivity(noActiveTab ? null : activeLink)
  2869.  
  2870. if (!tabsUiScript_setclick) {
  2871. tabsUiScript_setclick = true;
  2872. $(materialTab).on("click", "a", function(evt) {
  2873.  
  2874. let ytdFlexyElm = kRef(ytdFlexy);
  2875. if (!scriptEnable || !ytdFlexyElm) return null;
  2876.  
  2877. if (!this.hasAttribute('userscript-tab-content')) return;
  2878.  
  2879.  
  2880. evt.preventDefault();
  2881.  
  2882.  
  2883. if (this.getAttribute('userscript-tab-content') == '#tab-comments' && parseInt(ytdFlexyElm.getAttribute('tabview-youtube-comments') || '') < 0) {
  2884. return;
  2885. }
  2886.  
  2887. new Promise(requestAnimationFrame).then(() => {
  2888.  
  2889.  
  2890. layoutStatusMutex.lockWith(unlock => {
  2891.  
  2892. switchTabActivity_lastTab = this.getAttribute('userscript-tab-content');
  2893.  
  2894. let isActiveAndVisible = this.classList.contains('tab-btn') && this.classList.contains('active') && !this.classList.contains('tab-btn-hidden')
  2895.  
  2896. if (isWideScreenWithTwoColumns() && !isTheater() && isActiveAndVisible) {
  2897. //optional
  2898. setTimeout(unlock, 80);
  2899. switchTabActivity(null);
  2900. ytBtnSetTheater();
  2901. } else if (isActiveAndVisible) {
  2902. setTimeout(unlock, 80);
  2903. switchTabActivity(null);
  2904. } else {
  2905. let pInterval = 60;
  2906. if (isChatExpand() && isWideScreenWithTwoColumns()) {
  2907. ytBtnCollapseChat();
  2908. } else if (isEngagementPanelExpanded() && isWideScreenWithTwoColumns()) {
  2909. ytBtnCloseEngagementPanels();
  2910. } else if (isWideScreenWithTwoColumns() && isTheater() && !isFullScreen()) {
  2911. ytBtnCancelTheater();
  2912. } else {
  2913. pInterval = 20;
  2914. }
  2915. setTimeout(() => {
  2916. setTimeout(makeBodyScroll, 20); // this is to make the image render
  2917.  
  2918. setTimeout(() => {
  2919. let rightTabs = document.querySelector('#right-tabs');
  2920. if (!isWideScreenWithTwoColumns() && rightTabs && rightTabs.offsetTop > 0 && this.classList.contains('active')) {
  2921. let tabButtonBar = document.querySelector('#material-tabs');
  2922. let tabButtonBarHeight = tabButtonBar ? tabButtonBar.offsetHeight : 0;
  2923. window.scrollTo(0, rightTabs.offsetTop - tabButtonBarHeight);
  2924. }
  2925. }, 60)
  2926.  
  2927. setTimeout(unlock, 80)
  2928. switchTabActivity(this)
  2929.  
  2930. }, pInterval);
  2931. }
  2932.  
  2933.  
  2934. })
  2935.  
  2936. })
  2937.  
  2938.  
  2939.  
  2940.  
  2941. });
  2942.  
  2943. }
  2944.  
  2945. }
  2946.  
  2947.  
  2948. // ---------------------------------------------------------------------------------------------
  2949. window.addEventListener("yt-navigate-finish", onNavigationEnd)
  2950.  
  2951. document.addEventListener("loadstart", (evt) => {
  2952. if (!evt || !evt.target || evt.target.nodeName !== "VIDEO") return;
  2953. let elm = evt.target;
  2954. if (!elm.matches('#player video, #movie_player video, video[tabview-mainvideo]')) return;
  2955.  
  2956. let src = elm.src;
  2957. if (src !== lastVideoURL) {
  2958. lastVideoURL = elm.src;
  2959.  
  2960. elm.setAttribute('tabview-mainvideo', ''); // mainly for mini playing
  2961.  
  2962.  
  2963. }
  2964.  
  2965. }, true)
  2966.  
  2967. // ---------------------------------------------------------------------------------------------
  2968.  
  2969. var scrolling_lastD = 0;
  2970.  
  2971. const singleColumnScrolling = function(scrolling_lastF) {
  2972. let pageY = pageYOffset;
  2973. if (pageY < 10 && scrolling_lastD === 0 && !scrolling_lastF) return;
  2974.  
  2975. let targetElm, header, navElm;
  2976.  
  2977. Promise.resolve().then(() => {
  2978.  
  2979. targetElm = document.querySelector("#right-tabs");
  2980. if (!targetElm) return;
  2981. header = targetElm.querySelector("header");
  2982. if (!header) return;
  2983. navElm = document.querySelector('#masthead-container, #masthead')
  2984. if (!navElm) return;
  2985. let navHeight = navElm ? navElm.offsetHeight : 0
  2986.  
  2987. let elmY = targetElm.offsetTop
  2988.  
  2989. let xyz = [elmY + navHeight, pageY, elmY - navHeight]
  2990.  
  2991. let xyStatus = 0
  2992. if (xyz[1] < xyz[2] && xyz[2] < xyz[0]) {
  2993. // 1
  2994. xyStatus = 1
  2995. }
  2996.  
  2997. if (xyz[0] > xyz[1] && xyz[1] > xyz[2]) {
  2998.  
  2999. //2
  3000. xyStatus = 2
  3001.  
  3002. }
  3003.  
  3004. if (xyz[2] < xyz[0] && xyz[0] < xyz[1]) {
  3005. // 3
  3006.  
  3007. xyStatus = 3
  3008.  
  3009.  
  3010. }
  3011.  
  3012. return xyStatus;
  3013.  
  3014. }).then((xyStatus) => {
  3015.  
  3016. if ((xyStatus == 2 || xyStatus == 3) && (scrolling_lastD === 0 || scrolling_lastF)) {
  3017. scrolling_lastD = 1;
  3018. let {
  3019. offsetHeight
  3020. } = header
  3021. let {
  3022. offsetWidth
  3023. } = targetElm
  3024.  
  3025. targetElm.style.setProperty("--userscript-sticky-width", offsetWidth + 'px')
  3026. targetElm.style.setProperty("--userscript-sticky", offsetHeight + 'px')
  3027.  
  3028. wAttr(targetElm, 'userscript-sticky', true);
  3029.  
  3030. } else if ((xyStatus == 1) && (scrolling_lastD === 1 || scrolling_lastF)) {
  3031. scrolling_lastD = 0;
  3032.  
  3033. wAttr(targetElm, 'userscript-sticky', false);
  3034. }
  3035.  
  3036.  
  3037. targetElm = null;
  3038. header = null;
  3039. navElm = null;
  3040.  
  3041. });
  3042.  
  3043. };
  3044.  
  3045. window.addEventListener("scroll", function() {
  3046. if (!scriptEnable) return;
  3047. singleColumnScrolling(false)
  3048. }, {
  3049. capture: false,
  3050. passive: true
  3051. })
  3052.  
  3053. var lastResizeAt = 0;
  3054. window.addEventListener('resize', function() {
  3055. if (!scriptEnable) return;
  3056. lastResizeAt = Date.now();
  3057.  
  3058. requestAnimationFrame(() => {
  3059. if (!scriptEnable) return;
  3060. singleColumnScrolling(true)
  3061. })
  3062.  
  3063. }, {
  3064. capture: false,
  3065. passive: true
  3066. })
  3067.  
  3068. window.addEventListener('beforeunload', function() {
  3069. if (!scriptEnable) return;
  3070. console.log('beforeunload')
  3071. pluginUnhook();
  3072. //let video=document.querySelector('video');
  3073. //if(video && !video.paused) video.pause();
  3074. }, { capture: true })
  3075.  
  3076. window.addEventListener('hashchange', function() {
  3077. if (!scriptEnable) return;
  3078. console.log('hashchange')
  3079. pluginUnhook();
  3080. }, { capture: true })
  3081.  
  3082. window.addEventListener('popstate', function() {
  3083. if (!scriptEnable) return;
  3084. console.log('popstate')
  3085. pluginUnhook();
  3086. }, { capture: true })
  3087.  
  3088.  
  3089. function clearMutationObserver(o, key) {
  3090. if (o[key]) {
  3091. o[key].takeRecords();
  3092. o[key].disconnect();
  3093. o[key] = null;
  3094. return true;
  3095. }
  3096. }
  3097.  
  3098. function initMutationObserver(o, key, callback) {
  3099. clearMutationObserver(o, key);
  3100. const mto = new MutationObserver(callback);
  3101. o[key] = mto;
  3102. return mto;
  3103. }
  3104.  
  3105. document.addEventListener('wheel', function(evt) {
  3106.  
  3107. if (!scriptEnable) return;
  3108. const displayedPlaylist_element = kRef(displayedPlaylist);
  3109. if (displayedPlaylist_element && displayedPlaylist_element.contains(evt.target)) {
  3110. evt.stopPropagation();
  3111. evt.stopImmediatePropagation();
  3112. }
  3113. }, { capture: true, passive: true });
  3114.  
  3115.  
  3116. function setVideosTwoColumns(flag, bool) {
  3117.  
  3118. //two columns to one column
  3119.  
  3120. /*
  3121. [placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy
  3122.  
  3123. is-two-columns ="" => no is-two-columns
  3124. [placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer
  3125. no hidden => hidden =""
  3126. [placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer
  3127. hidden ="" => no hidden
  3128.  
  3129. */
  3130.  
  3131. let cssSelector1 = '[placeholder-videos] ytd-watch-next-secondary-results-renderer.style-scope.ytd-watch-flexy'
  3132.  
  3133. let cssSelector2 = '[placeholder-videos] tp-yt-paper-spinner#spinner.style-scope.ytd-continuation-item-renderer'
  3134.  
  3135. let cssSelector3 = '[placeholder-videos] div#button.style-scope.ytd-continuation-item-renderer'
  3136.  
  3137. let res = {}
  3138. if (flag & 1) {
  3139. res.m1 = document.querySelector(cssSelector1)
  3140. if (res.m1) wAttr(res.m1, 'is-two-columns', bool ? '' : false);
  3141. }
  3142.  
  3143. if (flag & 2) {
  3144. res.m2 = document.querySelector(cssSelector2)
  3145. if (res.m2) wAttr(res.m2, 'hidden', bool ? false : '');
  3146. }
  3147.  
  3148. if (flag & 4) {
  3149. res.m3 = document.querySelector(cssSelector3)
  3150. if (res.m3) wAttr(res.m3, 'hidden', bool ? '' : false);
  3151. }
  3152.  
  3153.  
  3154. return res
  3155.  
  3156.  
  3157.  
  3158.  
  3159. }
  3160.  
  3161. let lastScrollFetch = 0;
  3162. // function isScrolledToEnd(){
  3163. // return (window.innerHeight + window.pageYOffset) >= document.scrollingElement.scrollHeight - 2;
  3164. // }
  3165. let lastOffsetTop = 0;
  3166. window.addEventListener('scroll', function(evt) {
  3167.  
  3168. //console.log(evt.target)
  3169.  
  3170. if (!scriptEnable) return;
  3171.  
  3172.  
  3173. if (!kRef(scrollingVideosList)) return;
  3174. if (videoListBeforeSearch) return;
  3175.  
  3176.  
  3177.  
  3178. let visibleHeight = document.scrollingElement.clientHeight;
  3179. let totalHeight = document.scrollingElement.scrollHeight;
  3180.  
  3181. if (totalHeight < visibleHeight * 1.5) return; // filter out two column view;
  3182.  
  3183. let z = window.pageYOffset + visibleHeight;
  3184. let h_advanced = totalHeight - (visibleHeight > 5 * 40 ? visibleHeight * 0.5 : 40);
  3185.  
  3186.  
  3187.  
  3188. if (z > h_advanced && !isWideScreenWithTwoColumns()) {
  3189.  
  3190. let ct = Date.now();
  3191. if (ct - lastScrollFetch < 500) return; //prevent continuous calling
  3192.  
  3193. lastScrollFetch = ct;
  3194.  
  3195. let res = setVideosTwoColumns(2 | 4, true)
  3196. if (res.m3 && res.m2) {
  3197.  
  3198. //wait for DOM change, just in case
  3199. requestAnimationFrame(() => {
  3200. let { offsetTop } = res.m2 // as visibility of m2 & m3 switched.
  3201.  
  3202. if (offsetTop - lastOffsetTop < 40) return; // in case bug, or repeating calling. // the next button shall below the this button
  3203. lastOffsetTop = offsetTop
  3204.  
  3205. res.m2.parentNode.dispatchEvent(new Event('yt-service-request-sent-button-renderer'))
  3206.  
  3207. res = null
  3208. })
  3209.  
  3210. } else {
  3211.  
  3212. res = null
  3213. }
  3214.  
  3215.  
  3216. }
  3217.  
  3218.  
  3219.  
  3220.  
  3221. }, { passive: true })
  3222.  
  3223.  
  3224.  
  3225.  
  3226. /* --------------------------- browser's bug in -webkit-box ----------------------------------------- */
  3227.  
  3228. /*
  3229. fix bug for comment section - version 1.8.7
  3230. This issue is the bug in browser's rendering
  3231. I guess, this is due to the lines clamp with display:-webkit-box
  3232. use stupid coding to let it re-render when its content become visible
  3233. /*
  3234.  
  3235. ytd-expander[should-use-number-of-lines][collapsed] > #content.ytd-expander {
  3236. color: var(--yt-spec-text-primary);
  3237. display: -webkit-box;
  3238. overflow: hidden;
  3239. max-height: none;
  3240. -webkit-box-orient: vertical;
  3241. -webkit-line-clamp: var(--ytd-expander-max-lines, 4);
  3242. }
  3243.  
  3244. // v1.8.36 imposed a effective solution for fixing this bug
  3245.  
  3246. */
  3247.  
  3248. /* --------------------------- browser's bug in -webkit-box ----------------------------------------- */
  3249.  
  3250.  
  3251.  
  3252.  
  3253. })();
  3254.  
  3255.  
  3256.  
  3257.  
  3258.  
  3259.  
  3260.  
  3261.  
  3262.  
  3263.  
  3264.  
  3265.  
  3266.  
  3267.  
  3268.  
  3269.  
  3270.  
  3271.  
  3272.  
  3273.  
  3274.  
  3275.  
  3276.  
  3277.  
  3278.  
  3279.  
  3280.  
  3281.  
  3282.  
  3283.  
  3284.  
  3285.  
  3286.  
  3287.  
  3288.  
  3289.  
  3290.  
  3291.  
  3292.  
  3293.  
  3294.  
  3295.  
  3296. // https://github.com/cyfung1031/Tabview-Youtube/raw/main/js/content.js
  3297.  
  3298. }
  3299.  
  3300.  
  3301. ;!(function $$() {
  3302. 'use strict';
  3303.  
  3304. if(document.documentElement==null) return window.requestAnimationFrame($$)
  3305.  
  3306. const cssTxt = GM_getResourceText("contentCSS");
  3307.  
  3308. function addStyle (styleText) {
  3309. const styleNode = document.createElement('style');
  3310. styleNode.type = 'text/css';
  3311. styleNode.textContent = styleText;
  3312. document.documentElement.appendChild(styleNode);
  3313. return styleNode;
  3314. }
  3315.  
  3316. addStyle (cssTxt);
  3317.  
  3318. main(window.$);
  3319.  
  3320.  
  3321. // Your code here...
  3322. })();