YouTube 超快聊天

让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。

当前为 2023-07-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @name:ja YouTube スーパーファーストチャット
  4. // @name:zh-TW YouTube 超快聊天
  5. // @name:zh-CN YouTube 超快聊天
  6. // @namespace UserScript
  7. // @match https://www.youtube.com/live_chat*
  8. // @version 0.2.0
  9. // @license MIT
  10. // @author CY Fung
  11. // @run-at document-start
  12. // @grant none
  13. // @unwrap
  14. // @allFrames true
  15. // @inject-into page
  16. //
  17. // @description To make your YouTube Live Chat scroll instantly without smoothing transform CSS
  18. // @description:ja YouTubeライブチャットをスムーズな変形CSSなしで瞬時にスクロールさせるために。
  19. // @description:zh-TW 讓您的 YouTube 直播聊天即時滾動,不經過平滑轉換 CSS。
  20. // @description:zh-CN 让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。
  21. //
  22. // ==/UserScript==
  23.  
  24. ((__CONTEXT__) => {
  25.  
  26. const ACTIVE_DEFERRED_APPEND = false; // somehow buggy
  27. const ACTIVE_CONTENT_VISIBILITY = true;
  28. const ACTIVE_CONTAIN_SIZE = true;
  29. const addCss = () => document.head.appendChild(document.createElement('style')).textContent = [
  30. !ACTIVE_CONTENT_VISIBILITY ? '' : `
  31. @supports (contain:layout paint style) and (content-visibility:auto) and (contain-intrinsic-size:auto var(--wsr94)) {
  32. [wSr93] {
  33. content-visibility: visible;
  34. }
  35. [wSr93="hidden"]:nth-last-child(n+4) {
  36. content-visibility: auto;
  37. contain-intrinsic-size: auto var(--wsr94);
  38. }
  39. }
  40. `,
  41. !ACTIVE_CONTAIN_SIZE ? '' : `
  42. @supports (contain:layout paint style) {
  43. [wSr93*="i"] {
  44. height: var(--wsr94);
  45. box-sizing: border-box;
  46. contain: size layout style;
  47. }
  48. }
  49. `,
  50. `
  51. @supports (contain:layout paint style) {
  52. [wSr93*="i"] {
  53. height: var(--wsr94);
  54. box-sizing: border-box;
  55. contain: size layout style;
  56. }
  57. /* optional */
  58. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  59. height: auto !important;
  60. min-height: unset !important;
  61. }
  62. #items.style-scope.yt-live-chat-item-list-renderer {
  63. transform: translateY(0px) !important; /*padding-bottom: 0 !important;
  64. padding-top: 0 !important;*/
  65. }
  66. /* optional */
  67. yt-icon[icon="down_arrow"] > *, yt-icon-button#show-more > * {
  68. pointer-events: none !important;
  69. }
  70. #item-list.style-scope.yt-live-chat-renderer, yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer, #item-list.style-scope.yt-live-chat-renderer *, yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer * {
  71. will-change: unset !important;
  72. }
  73. yt-img-shadow[height][width] {
  74. content-visibility: visible !important;
  75. }
  76. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer {
  77. position: static !important;
  78. }
  79. /* ------------------------------------------------------------------------------------------------------------- */
  80. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip, yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer, yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image, yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  81. contain: layout style;
  82. }
  83. /*
  84. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
  85. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
  86. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
  87. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  88. contain: layout style;
  89. display: inline-flex;
  90. vertical-align: middle;
  91. }
  92. */
  93. #items yt-live-chat-text-message-renderer {
  94. contain: layout style;
  95. }
  96. yt-live-chat-item-list-renderer:not([allow-scroll]) #item-scroller.yt-live-chat-item-list-renderer {
  97. overflow-y: scroll;
  98. padding-right: 0;
  99. }
  100. body yt-live-chat-app {
  101. contain: size layout paint style;
  102. overflow: hidden;
  103. }
  104. #items.style-scope.yt-live-chat-item-list-renderer {
  105. contain: layout paint style;
  106. }
  107. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  108. contain: style;
  109. }
  110. #item-scroller.style-scope.yt-live-chat-item-list-renderer {
  111. contain: size style;
  112. }
  113. #contents.style-scope.yt-live-chat-item-list-renderer, #chat.style-scope.yt-live-chat-renderer, img.style-scope.yt-img-shadow[width][height] {
  114. contain: size layout paint style;
  115. }
  116. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label], .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label] > #container {
  117. contain: layout paint style;
  118. }
  119. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer, yt-live-chat-membership-item-renderer.style-scope.yt-live-chat-item-list-renderer, yt-live-chat-paid-message-renderer.style-scope.yt-live-chat-item-list-renderer, yt-live-chat-banner-manager.style-scope.yt-live-chat-item-list-renderer {
  120. contain: layout style;
  121. }
  122. tp-yt-paper-tooltip[style*="inset"][role="tooltip"] {
  123. contain: layout paint style;
  124. }
  125. /*
  126. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  127. position: relative !important;
  128. height: auto !important;
  129. }
  130. */
  131. /* ------------------------------------------------------------------------------------------------------------- */
  132. #items.style-scope.yt-live-chat-item-list-renderer {
  133. padding-top: var(--items-top-padding);
  134. }
  135. #continuations, #continuations * {
  136. contain: strict;
  137. position: fixed;
  138. top: 2px;
  139. height: 1px;
  140. width: 2px;
  141. height: 1px;
  142. visibility: collapse;
  143. }
  144. }
  145. `
  146. ].join('\n');
  147. const { Promise, requestAnimationFrame } = __CONTEXT__;
  148. const isContainSupport = CSS.supports('contain', 'layout paint style');
  149. if (!isContainSupport) {
  150. console.error(`
  151. YouTube Light Chat Scroll: Your browser does not support 'contain'.
  152. Chrome >= 52; Edge >= 79; Safari >= 15.4, Firefox >= 69; Opera >= 39
  153. `.trim());
  154. return;
  155. }
  156. // const APPLY_delayAppendChild = false;
  157. let activeDeferredAppendChild = false;
  158. let delayedAppendParentWS = new WeakSet();
  159. let delayedAppendOperations = [];
  160. let commonAppendParentStackSet = new Set();
  161. const sp7 = Symbol();
  162. let dt0 = Date.now() - 2000;
  163. const dateNow = () => Date.now() - dt0;
  164. let lastScroll = 0;
  165. let lastLShow = 0;
  166. let lastWheel = 0;
  167. const proxyHelperFn = (dummy) => ({
  168. get(target, prop) {
  169. return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
  170. },
  171. set(target, prop, value) {
  172. if (!(prop in dummy)) {
  173. target[prop] = value;
  174. }
  175. return true;
  176. },
  177. has(target, prop) {
  178. return (prop in target)
  179. },
  180. deleteProperty(target, prop) {
  181. return true;
  182. },
  183. ownKeys(target) {
  184. return Object.keys(target);
  185. },
  186. defineProperty(target, key, descriptor) {
  187. return Object.defineProperty(target, key, descriptor);
  188. // return true;
  189. },
  190. getOwnPropertyDescriptor(target, key) {
  191. return Object.getOwnPropertyDescriptor(target, key);
  192. },
  193. });
  194. // const dummy3v = {
  195. // "background": "",
  196. // "backgroundAttachment": "",
  197. // "backgroundBlendMode": "",
  198. // "backgroundClip": "",
  199. // "backgroundColor": "",
  200. // "backgroundImage": "",
  201. // "backgroundOrigin": "",
  202. // "backgroundPosition": "",
  203. // "backgroundPositionX": "",
  204. // "backgroundPositionY": "",
  205. // "backgroundRepeat": "",
  206. // "backgroundRepeatX": "",
  207. // "backgroundRepeatY": "",
  208. // "backgroundSize": ""
  209. // };
  210. // for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  211. // dummy3v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  212. // }
  213. // const dummy3p = phFn(dummy3v);
  214. const pt2DecimalFixer = (x) => Math.round(x * 5, 0) / 5;
  215. const tickerContainerSetAttribute = function (attrName, attrValue) {
  216. let yd = (this.__dataHost || 0).__data;
  217. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  218. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  219. let v = `${attrValue}`;
  220. // conside a ticker is 101px width
  221. // 1% = 1.01px
  222. // 0.2% = 0.202px
  223. const ratio1 = (yd.ratio * 100);
  224. const ratio2 = pt2DecimalFixer(ratio1);
  225. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  226. if (yd.__style_last__ === v) return;
  227. yd.__style_last__ = v;
  228. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  229. } else {
  230. HTMLElement.prototype.setAttribute.apply(this, arguments);
  231. }
  232. };
  233. /*
  234. *
  235. * const tickerContainerSetAttribute = function (attrName, attrValue) {
  236. const yd = (this.__dataHost||0).__data;
  237. if (arguments.length === 2 && attrName === 'style' && attrValue && yd){
  238. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  239. let v = attrValue;
  240. // conside a ticker is 101px width
  241. // 1% = 1.01px
  242. // 0.2% = 0.202px
  243. const ratio1 = (yd.ratio * 100);
  244. const ratio2 = pt2DecimalFixer(ratio1);
  245. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  246. console.log(ratio1, ratio2)
  247. if (yd.__style_last__ !== v) {
  248. yd.__style_last__ = v; // clear along with data change
  249. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  250. return;
  251. }
  252. }
  253. return HTMLElement.prototype.setAttribute.apply(this, arguments);
  254. };
  255. */
  256. const createDelayAppendOper = () => requestAnimationFrame(() => {
  257. const e = [...delayedAppendOperations]
  258. delayedAppendOperations.length = 0;
  259. for (const t of e) t();
  260. });
  261. Node.prototype.appendChild = ((appendChild) => (function (s) {
  262. if (arguments.length !== 1) return appendChild.apply(this, arguments);
  263. // console.log(34, 1, this.is, this.nodeName, activeDeferredAppendChild, s.nodeName)
  264. const stack = new Error().stack;
  265. if (ACTIVE_DEFERRED_APPEND && activeDeferredAppendChild && (commonAppendParentStackSet.has(stack) || s.nodeName === 'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER') && typeof s.is === 'string') {
  266. commonAppendParentStackSet.add(stack);
  267. // this = '#document-fragment'
  268. /*
  269. if (this instanceof HTMLElement) {
  270. if (ops.length === 0) createRAF();
  271. ops.push(() => {
  272. appendChild.apply(this, arguments);
  273. })
  274. return s;
  275. } else {
  276. mpws.add(this);
  277. appendChild.apply(this, arguments);
  278. return s;
  279. }
  280. */
  281. delayedAppendParentWS.add(this);
  282. if (delayedAppendOperations.length === 0) createDelayAppendOper();
  283. delayedAppendOperations.push(() => {
  284. delayedAppendParentWS.delete(this);
  285. appendChild.apply(this, arguments);
  286. })
  287. return s;
  288. } else if (ACTIVE_DEFERRED_APPEND && activeDeferredAppendChild && delayedAppendParentWS.has(s)) {
  289. /*
  290. if (this instanceof HTMLElement) {
  291. if (ops.length === 0) createRAF();
  292. ops.push(() => {
  293. mpws.delete(s);
  294. appendChild.apply(this, arguments);
  295. })
  296. return s;
  297. } else {
  298. mpws.delete(s);
  299. appendChild.apply(this, arguments);
  300. return s;
  301. }
  302. */
  303. if (delayedAppendOperations.length === 0) createDelayAppendOper();
  304. delayedAppendOperations.push(() => {
  305. delayedAppendParentWS.delete(s);
  306. appendChild.apply(this, arguments);
  307. })
  308. return s;
  309. } else if (this.nodeName === 'YT-LIVE-CHAT-TICKER-PAID-MESSAGE-ITEM-RENDERER') {
  310. appendChild.call(this, s);
  311. let container = this.$.container;
  312. if (container) {
  313. // const sp3v = new Proxy(container.style, dummy3p)
  314. // Object.defineProperty(container, 'style', {get(){return sp3v}, set() { }, enumerable: true, configurable: true });
  315. container.setAttribute = tickerContainerSetAttribute;
  316. }
  317. return s;
  318. }
  319. // if(activeDeferredAppendChild) return null;
  320. appendChild.call(this, s);
  321. return s;
  322. }))(Node.prototype.appendChild);
  323. /*
  324. Node.prototype.append = ((append) => (function () {
  325. // console.log(34,2 )
  326. return append.apply(this, arguments);
  327. }))(Node.prototype.append);
  328. Node.prototype.insertBefore = ((insertBefore) => (function () {
  329. // console.log(34,3, this.is, this.nodeName, activeDeferredAppendChild)
  330. // if(activeDeferredAppendChild) return null;
  331. return insertBefore.apply(this, arguments);
  332. }))(Node.prototype.insertBefore);
  333. Node.prototype.insertAfter = ((insertAfter) => (function () {
  334. // console.log(34,4)
  335. return insertAfter.apply(this, arguments);
  336. }))(Node.prototype.insertAfter);
  337. */
  338. const fxOperator = (proto, propertyName) => {
  339. let propertyDescriptorGetter = null;
  340. try {
  341. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  342. } catch (e) { }
  343. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  344. };
  345. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  346. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  347. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  348. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  349. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  350. /* globals WeakRef:false */
  351. /** @type {(o: Object | null) => WeakRef | null} */
  352. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  353. /** @type {(wr: Object | null) => Object | null} */
  354. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  355. const watchUserCSS = () => {
  356. // if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;
  357. const clearContentVisibilitySizing = () => {
  358. Promise.resolve().then(() => {
  359. let btnShowMoreWR = mWeakRef(document.querySelector('#show-more[disabled]'));
  360. let lastVisibleItemWR = null;
  361. for (const elm of document.querySelectorAll('[wSr93]')) {
  362. if (elm.getAttribute('wSr93') === 'visible') lastVisibleItemWR = mWeakRef(elm);
  363. elm.setAttribute('wSr93', '');
  364. // custom CSS property --wsr94 not working when attribute wSr93 removed
  365. }
  366. requestAnimationFrame(() => {
  367. const btnShowMore = kRef(btnShowMoreWR); btnShowMoreWR = null;
  368. if (btnShowMore && btnShowMore.isConnected) btnShowMore.click();
  369. else {
  370. // would not work if switch it frequently
  371. const lastVisibleItem = kRef(lastVisibleItemWR); lastVisibleItemWR = null;
  372. if (lastVisibleItem && lastVisibleItem.isConnected) {
  373. Promise.resolve()
  374. .then(() => lastVisibleItem.scrollIntoView())
  375. .then(() => lastVisibleItem.scrollIntoView(false))
  376. .then(() => lastVisibleItem.scrollIntoView({ behavior: "instant", block: "end", inline: "nearest" }))
  377. .catch(e => { }) // break the chain when method not callable
  378. }
  379. }
  380. })
  381. })
  382. }
  383. const mutObserver = new MutationObserver((mutations) => {
  384. console.log(mutations.length)
  385. for (const mutation of mutations) {
  386. if ((mutation.addedNodes || 0).length >= 1) {
  387. for (const addedNode of mutation.addedNodes) {
  388. if (addedNode.nodeName === 'STYLE') {
  389. clearContentVisibilitySizing();
  390. return;
  391. }
  392. }
  393. }
  394. if ((mutation.removedNodes || 0).length >= 1) {
  395. for (const removedNode of mutation.removedNodes) {
  396. if (removedNode.nodeName === 'STYLE') {
  397. clearContentVisibilitySizing();
  398. return;
  399. }
  400. }
  401. }
  402. }
  403. });
  404. mutObserver.observe(document.documentElement, {
  405. childList: true,
  406. subtree: false
  407. })
  408. mutObserver.observe(document.head, {
  409. childList: true,
  410. subtree: false
  411. })
  412. mutObserver.observe(document.body, {
  413. childList: true,
  414. subtree: false
  415. });
  416. }
  417. let done = 0;
  418. let main = async (q) => {
  419. if (done) return;
  420. if (!q) return;
  421. let m1 = nodeParent(q);
  422. let m2 = q;
  423. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  424. done = 1;
  425. Promise.resolve().then(watchUserCSS);
  426. addCss();
  427. const dummy1v = {
  428. transform: '',
  429. height: '',
  430. minHeight: '',
  431. paddingBottom: '',
  432. paddingTop: ''
  433. };
  434. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  435. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  436. }
  437. const dummy1p = proxyHelperFn(dummy1v);
  438. const sp1v = new Proxy(m1.style, dummy1p);
  439. const sp2v = new Proxy(m2.style, dummy1p);
  440. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  441. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  442. m1.removeAttribute("style");
  443. m2.removeAttribute("style");
  444. let lastClick = 0;
  445. document.addEventListener('click', (evt) => {
  446. if (!evt.isTrusted) return;
  447. const target = ((evt || 0).target || 0)
  448. if (target.id === 'show-more') {
  449. if (target.nodeName !== 'YT-ICON-BUTTON') return;
  450. if (dateNow() - lastClick < 80) return;
  451. requestAnimationFrame(() => {
  452. lastClick = dateNow();
  453. target.click();
  454. })
  455. }
  456. })
  457. let btnShowMoreWR = null;
  458. const clickShowMore = () => {
  459. let btnShowMore = kRef(btnShowMoreWR);
  460. if (!btnShowMore || !btnShowMore.isConnected) {
  461. btnShowMore = document.querySelector('#show-more.yt-live-chat-item-list-renderer');
  462. btnShowMoreWR = mWeakRef(btnShowMore);
  463. }
  464. if (btnShowMore) btnShowMore.click();
  465. };
  466. let hasFirstShowMore = false;
  467. const visObserver = new IntersectionObserver((entries) => {
  468. for (const entry of entries) {
  469. const target = entry.target;
  470. if (!target) continue;
  471. let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
  472. if (isVisible) {
  473. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  474. target.setAttribute('wSr93', 'visible');
  475. if (nNextElem(target) === null) {
  476. canSetMaxScrollTop = true;
  477. if (dateNow() - lastScroll < 80) {
  478. lastLShow = 0;
  479. lastScroll = 0;
  480. Promise.resolve().then(clickShowMore);
  481. } else {
  482. lastLShow = dateNow();
  483. }
  484. } else if (!hasFirstShowMore) { // should more than one item being visible
  485. // implement inside visObserver to ensure there is sufficient delay
  486. hasFirstShowMore = true;
  487. requestAnimationFrame(() => {
  488. // foreground page
  489. activeDeferredAppendChild = true;
  490. // page visibly ready -> load the latest comments at initial loading
  491. clickShowMore();
  492. });
  493. }
  494. }
  495. else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing
  496. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  497. target.setAttribute('wSr93', 'hidden');
  498. } // note: might consider 0 < entry.intersectionRatio < 0.5 and target.getAttribute('wSr93') === '' <new last item>
  499. }
  500. }, {
  501. /*
  502. root: items,
  503. rootMargin: "0px",
  504. threshold: 1.0,
  505. */
  506. root: document.querySelector('#item-scroller'), // nullable
  507. rootMargin: "0px",
  508. threshold: [0.05, 0.95],
  509. });
  510. //m2.style.visibility='';
  511. const mutFn = (items) => {
  512. let node = nLastElem(items);
  513. for (; node !== null; node = nPrevElem(node)) {
  514. if (node.hasAttribute('wSr93')) break;
  515. node.setAttribute('wSr93', '');
  516. visObserver.observe(node);
  517. }
  518. }
  519. const mutObserver = new MutationObserver((mutations) => {
  520. const items = (mutations[0] || 0).target;
  521. if (!items) return;
  522. mutFn(items);
  523. });
  524. mutObserver.observe(m2, {
  525. childList: true,
  526. subtree: false
  527. });
  528. mutFn(m2);
  529. /** @type {HTMLElement} */
  530. let c1 = nPrevElem(m1);
  531. if (c1 && c1.id === "live-chat-banner") {
  532. let rsObserver = new ResizeObserver((entries) => {
  533. for (const entry of entries) {
  534. const target = entry.target;
  535. if (target && target.id === "live-chat-banner") {
  536. let p = entry.borderBoxSize ? (entry.borderBoxSize[0] || 0).blockSize : 0;
  537. let c1h = p > entry.contentRect.height ? p : entry.contentRect.height + 16;
  538. document.documentElement.style.setProperty('--items-top-padding', (Math.ceil(c1h / 2) * 2) + 'px');
  539. }
  540. }
  541. });
  542. rsObserver.observe(c1);
  543. }
  544. let maxScrollTop = -1;
  545. let canSetMaxScrollTop = false;
  546. document.addEventListener('scroll', (evt) => {
  547. if (!evt || !evt.isTrusted) return;
  548. if (!canSetMaxScrollTop) return;
  549. const isUserAction = dateNow() - lastWheel < 80; // continuous wheel -> continuous scroll -> continuous wheel -> continuous scroll
  550. if (!isUserAction) return;
  551. if (dateNow() - lastLShow < 80) {
  552. lastLShow = 0;
  553. lastScroll = 0;
  554. Promise.resolve().then(clickShowMore);
  555. } else {
  556. lastScroll = dateNow();
  557. }
  558. }, { passive: true, capture: true }) // support contain => support passive
  559. document.addEventListener('wheel', (evt) => {
  560. if (!evt || !evt.isTrusted) return;
  561. lastWheel = dateNow();
  562. }, { passive: true, capture: true }) // support contain => support passive
  563. };
  564. function onReady() {
  565. let tmObserver = new MutationObserver(() => {
  566. let p = document.getElementById('items'); // fast
  567. if (!p) return;
  568. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer'); // check
  569. if (q) {
  570. tmObserver.disconnect();
  571. tmObserver.takeRecords();
  572. tmObserver = null;
  573. Promise.resolve(q).then((q) => {
  574. // confirm Promis.resolve() is resolveable
  575. // execute main without direct blocking
  576. main(q);
  577. })
  578. }
  579. });
  580. tmObserver.observe(document.body, {
  581. childList: true,
  582. subtree: true
  583. });
  584. }
  585. if (document.readyState != 'loading') {
  586. onReady();
  587. } else {
  588. window.addEventListener("DOMContentLoaded", onReady, false);
  589. }
  590. })({ Promise, requestAnimationFrame });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址