YouTube 超快聊天

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

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

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @version 0.5.16
  4. // @license MIT
  5. // @name:ja YouTube スーパーファーストチャット
  6. // @name:zh-TW YouTube 超快聊天
  7. // @name:zh-CN YouTube 超快聊天
  8. // @namespace UserScript
  9. // @match https://www.youtube.com/live_chat*
  10. // @author CY Fung
  11. // @require https://gf.qytechs.cn/scripts/465819-api-for-customelements-in-youtube/code/API%20for%20CustomElements%20in%20YouTube.js?version=1215280
  12. // @run-at document-start
  13. // @grant none
  14. // @unwrap
  15. // @allFrames true
  16. // @inject-into page
  17. //
  18. // @description To make your YouTube Live Chat scroll instantly without smoothing transform CSS
  19. // @description:ja YouTubeライブチャットをスムーズな変形CSSなしで瞬時にスクロールさせるために。
  20. // @description:zh-TW 讓您的 YouTube 直播聊天即時滾動,不經過平滑轉換 CSS。
  21. // @description:zh-CN 让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。
  22. //
  23. // ==/UserScript==
  24.  
  25. ((__CONTEXT__) => {
  26.  
  27. // const ACTIVE_DEFERRED_APPEND = false; // somehow buggy
  28.  
  29. // const ACTIVE_CONTENT_VISIBILITY = true;
  30. // const ACTIVE_CONTAIN_SIZE = true;
  31.  
  32. const addCss = () => document.head.appendChild(document.createElement('style')).textContent = `
  33.  
  34.  
  35. @supports (contain:layout paint style) and (content-visibility:auto) and (contain-intrinsic-size:auto var(--wsr94)) {
  36.  
  37. [wSr93="hidden"]:nth-last-child(n+4) {
  38. --wsr93-content-visibility: auto;
  39. contain-intrinsic-size: auto var(--wsr94);
  40. }
  41.  
  42. }
  43.  
  44. @supports (contain:layout paint style) {
  45.  
  46. [wSr93] {
  47. --wsr93-contain: layout style;
  48. contain: var(--wsr93-contain, unset) !important;
  49. box-sizing: border-box !important;
  50. content-visibility: var(--wsr93-content-visibility, visible);
  51. }
  52. [wSr93="hidden"] { /* initial->[wSr93]->[wSr93="visible"]->[wSr93="hidden"] => reliable rendered height */
  53. --wsr93-contain: size layout style;
  54. height: var(--wsr94);
  55. }
  56.  
  57.  
  58. /* ------------------------------------------------------------------------------------------------------------- */
  59.  
  60. 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 {
  61. contain: layout style;
  62. }
  63.  
  64. body yt-live-chat-app {
  65. contain: size layout paint style;
  66. overflow: hidden;
  67. }
  68.  
  69. #items.style-scope.yt-live-chat-item-list-renderer {
  70. contain: layout paint style;
  71. }
  72.  
  73. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  74. contain: style;
  75. }
  76.  
  77. #item-scroller.style-scope.yt-live-chat-item-list-renderer {
  78. contain: size style;
  79. }
  80.  
  81. #contents.style-scope.yt-live-chat-item-list-renderer, #chat.style-scope.yt-live-chat-renderer, img.style-scope.yt-img-shadow[width][height] {
  82. contain: size layout paint style;
  83. }
  84.  
  85. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label], .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label] > #container {
  86. contain: layout paint style;
  87. }
  88.  
  89. 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 {
  90. contain: layout style;
  91. }
  92.  
  93. tp-yt-paper-tooltip[style*="inset"][role="tooltip"] {
  94. contain: layout paint style;
  95. }
  96.  
  97. /* ------------------------------------------------------------------------------------------------------------- */
  98.  
  99. }
  100.  
  101. @supports (color: var(--general)) {
  102.  
  103. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer {
  104. position: static !important;
  105. }
  106.  
  107. .ytp-contextmenu[class],
  108. .toggle-button.tp-yt-paper-toggle-button[class],
  109. .yt-spec-touch-feedback-shape__fill[class],
  110. .fill.yt-interaction[class],
  111. .ytp-videowall-still-info-content[class],
  112. .ytp-suggestion-image[class] {
  113. will-change: unset !important;
  114. }
  115.  
  116. yt-img-shadow[height][width] {
  117. content-visibility: visible !important;
  118. }
  119.  
  120. yt-live-chat-item-list-renderer:not([allow-scroll]) #item-scroller.yt-live-chat-item-list-renderer {
  121. overflow-y: scroll;
  122. padding-right: 0;
  123. }
  124.  
  125.  
  126. /* optional */
  127. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  128. height: auto !important;
  129. min-height: unset !important;
  130. }
  131.  
  132. #items.style-scope.yt-live-chat-item-list-renderer {
  133. transform: translateY(0px) !important;
  134. }
  135.  
  136. /* optional */
  137. yt-icon[icon="down_arrow"] > *, yt-icon-button#show-more > * {
  138. pointer-events: none !important;
  139. }
  140.  
  141. #continuations, #continuations * {
  142. contain: strict;
  143. position: fixed;
  144. top: 2px;
  145. height: 1px;
  146. width: 2px;
  147. height: 1px;
  148. visibility: collapse;
  149. }
  150.  
  151. yt-live-chat-renderer[has-action-panel-renderer] #show-more.yt-live-chat-item-list-renderer{
  152. top: 4px;
  153. transition-property: top;
  154. bottom: unset;
  155. }
  156.  
  157. yt-live-chat-renderer[has-action-panel-renderer] #show-more.yt-live-chat-item-list-renderer[disabled]{
  158. top: -42px;
  159. }
  160.  
  161. }
  162.  
  163. `;
  164.  
  165. const { Promise, requestAnimationFrame, IntersectionObserver } = __CONTEXT__;
  166.  
  167.  
  168. const isContainSupport = CSS.supports('contain', 'layout paint style');
  169. if (!isContainSupport) {
  170. console.warn("Your browser does not support css property 'contain'.\nPlease upgrade to the latest version.".trim());
  171. }
  172.  
  173. // const APPLY_delayAppendChild = false;
  174.  
  175. // let activeDeferredAppendChild = false; // deprecated
  176.  
  177. // let delayedAppendParentWS = new WeakSet();
  178. // let delayedAppendOperations = [];
  179. // let commonAppendParentStackSet = new Set();
  180.  
  181. // let firstVisibleItemDetected = false; // deprecated
  182.  
  183. const sp7 = Symbol();
  184.  
  185.  
  186. let dt0 = Date.now() - 2000;
  187. const dateNow = () => Date.now() - dt0;
  188. // let lastScroll = 0;
  189. // let lastLShow = 0;
  190. let lastWheel = 0;
  191.  
  192. const proxyHelperFn = (dummy) => ({
  193.  
  194. get(target, prop) {
  195. return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
  196. },
  197. set(target, prop, value) {
  198. if (!(prop in dummy)) {
  199. target[prop] = value;
  200. }
  201. return true;
  202. },
  203. has(target, prop) {
  204. return (prop in target)
  205. },
  206. deleteProperty(target, prop) {
  207. return true;
  208. },
  209. ownKeys(target) {
  210. return Object.keys(target);
  211. },
  212. defineProperty(target, key, descriptor) {
  213. return Object.defineProperty(target, key, descriptor);
  214. },
  215. getOwnPropertyDescriptor(target, key) {
  216. return Object.getOwnPropertyDescriptor(target, key);
  217. },
  218.  
  219. });
  220.  
  221. const tickerContainerSetAttribute = function (attrName, attrValue) { // ensure '14.30000001%'.toFixed(1)
  222.  
  223. let yd = (this.__dataHost || (this.inst || 0).__dataHost).__data;
  224.  
  225. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  226.  
  227. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  228. let v = `${attrValue}`;
  229. // conside a ticker is 101px width
  230. // 1% = 1.01px
  231. // 0.2% = 0.202px
  232.  
  233.  
  234. const ratio1 = (yd.ratio * 100);
  235. if (ratio1 > -1) { // avoid NaN
  236.  
  237. // countdownDurationMs
  238. // 600000 - 0.2% <1% = 6s> <0.2% = 1.2s>
  239. // 300000 - 0.5% <1% = 3s> <0.5% = 1.5s>
  240. // 150000 - 1% <1% = 1.5s>
  241. // 75000 - 2% <1% =0.75s > <2% = 1.5s>
  242. // 30000 - 5% <1% =0.3s > <5% = 1.5s>
  243. // 99px * 5% = 4.95px
  244.  
  245. // 15000 - 10% <1% =0.15s > <10% = 1.5s>
  246.  
  247.  
  248.  
  249.  
  250. // 1% Duration
  251.  
  252. let ratio2 = ratio1;
  253.  
  254. const ydd = yd.data;
  255. const d1 = ydd.durationSec;
  256. const d2 = ydd.fullDurationSec;
  257.  
  258. if(d1 === d2 && d1>1){
  259.  
  260. if(d1 > 400) ratio2 = Math.round(ratio2 * 5) / 5; // 0.2%
  261. else if(d1>200) ratio2 = Math.round(ratio2 * 2) / 2; // 0.5%
  262. else if(d1>100) ratio2 = Math.round(ratio2 * 1) / 1; // 1%
  263. else if(d1>50) ratio2 = Math.round(ratio2 * 0.5) / 0.5; // 2%
  264. else if(d1>25) ratio2 = Math.round(ratio2 * 0.2) / 0.2; // 5% (max => 99px * 5% = 4.95px)
  265. else ratio2 = Math.round(ratio2 * 0.2) / 0.2;
  266. }else {
  267. ratio2 = Math.round(ratio2 * 5) / 5; // 0.2% (min)
  268. }
  269. // ratio2 = Math.round(ratio2 * 5) / 5;
  270. ratio2 = ratio2.toFixed(1)
  271. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  272.  
  273. if (yd.__style_last__ === v) return;
  274. yd.__style_last__ = v;
  275. // do not consider any delay here.
  276. // it shall be inside the looping for all properties changes. all the css background ops are in the same microtask.
  277.  
  278. }
  279.  
  280. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  281.  
  282.  
  283. } else {
  284. HTMLElement.prototype.setAttribute.apply(this, arguments);
  285. }
  286.  
  287. };
  288.  
  289. const fxOperator = (proto, propertyName) => {
  290. let propertyDescriptorGetter = null;
  291. try {
  292. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  293. } catch (e) { }
  294. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  295. };
  296.  
  297. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  298. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  299. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  300. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  301. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  302.  
  303.  
  304. /* globals WeakRef:false */
  305.  
  306. /** @type {(o: Object | null) => WeakRef | null} */
  307. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  308.  
  309. /** @type {(wr: Object | null) => Object | null} */
  310. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  311.  
  312. const watchUserCSS = () => {
  313.  
  314. // if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;
  315.  
  316. const getElemFromWR = (nr) => {
  317. const n = kRef(nr);
  318. if (n && n.isConnected) return n;
  319. return null;
  320. }
  321.  
  322. const clearContentVisibilitySizing = () => {
  323. Promise.resolve().then(() => {
  324.  
  325. let btnShowMoreWR = mWeakRef(document.querySelector('#show-more[disabled]'));
  326.  
  327. let lastVisibleItemWR = null;
  328. for (const elm of document.querySelectorAll('[wSr93]')) {
  329. if (elm.getAttribute('wSr93') === 'visible') lastVisibleItemWR = mWeakRef(elm);
  330. elm.setAttribute('wSr93', '');
  331. // custom CSS property --wsr94 not working when attribute wSr93 removed
  332. }
  333. requestAnimationFrame(() => {
  334. const btnShowMore = getElemFromWR(btnShowMoreWR); btnShowMoreWR = null;
  335. if (btnShowMore) btnShowMore.click();
  336. else {
  337. // would not work if switch it frequently
  338. const lastVisibleItem = getElemFromWR(lastVisibleItemWR); lastVisibleItemWR = null;
  339. if (lastVisibleItem) {
  340.  
  341. Promise.resolve()
  342. .then(() => lastVisibleItem.scrollIntoView())
  343. .then(() => lastVisibleItem.scrollIntoView(false))
  344. .then(() => lastVisibleItem.scrollIntoView({ behavior: "instant", block: "end", inline: "nearest" }))
  345. .catch(e => { }) // break the chain when method not callable
  346.  
  347. }
  348. }
  349. })
  350.  
  351. })
  352.  
  353. }
  354.  
  355. const mutObserver = new MutationObserver((mutations) => {
  356. for (const mutation of mutations) {
  357. if ((mutation.addedNodes || 0).length >= 1) {
  358. for (const addedNode of mutation.addedNodes) {
  359. if (addedNode.nodeName === 'STYLE') {
  360. clearContentVisibilitySizing();
  361. return;
  362. }
  363. }
  364. }
  365. if ((mutation.removedNodes || 0).length >= 1) {
  366. for (const removedNode of mutation.removedNodes) {
  367. if (removedNode.nodeName === 'STYLE') {
  368. clearContentVisibilitySizing();
  369. return;
  370. }
  371. }
  372. }
  373. }
  374. });
  375.  
  376. mutObserver.observe(document.documentElement, {
  377. childList: true,
  378. subtree: false
  379. })
  380.  
  381. mutObserver.observe(document.head, {
  382. childList: true,
  383. subtree: false
  384. })
  385. mutObserver.observe(document.body, {
  386. childList: true,
  387. subtree: false
  388. });
  389.  
  390. }
  391.  
  392. const setupStyle = (m1, m2) => {
  393.  
  394. const dummy1v = {
  395. transform: '',
  396. height: '',
  397. minHeight: '',
  398. paddingBottom: '',
  399. paddingTop: ''
  400. };
  401. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  402. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  403. }
  404.  
  405. const dummy1p = proxyHelperFn(dummy1v);
  406. const sp1v = new Proxy(m1.style, dummy1p);
  407. const sp2v = new Proxy(m2.style, dummy1p);
  408. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  409. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  410. m1.removeAttribute("style");
  411. m2.removeAttribute("style");
  412.  
  413. }
  414.  
  415.  
  416. class WillChangeController {
  417. constructor(itemScroller, willChangeValue) {
  418. this.element = itemScroller;
  419. this.counter = 0;
  420. this.active = false;
  421. this.willChangeValue = willChangeValue;
  422. }
  423.  
  424. beforeOper() {
  425. if (!this.active) {
  426. this.active = true;
  427. this.element.style.willChange = this.willChangeValue;
  428. }
  429. this.counter++;
  430. }
  431.  
  432. afterOper() {
  433. const c = this.counter;
  434. requestAnimationFrame(() => {
  435. if (c === this.counter) {
  436. this.active = false;
  437. this.element.style.willChange = '';
  438. }
  439. })
  440. }
  441.  
  442. release() {
  443. const element = this.element;
  444. this.element = null;
  445. this.counter = 1e16;
  446. this.active = false;
  447. try {
  448. element.style.willChange = '';
  449. } catch (e) { }
  450. }
  451.  
  452. }
  453.  
  454.  
  455. customYtElements.onRegistryReady(() => {
  456.  
  457. let scrollWillChangeController = null;
  458. let contensWillChangeController = null;
  459.  
  460. // as it links to event handling, it has to be injected using immediateCallback
  461. customYtElements.whenRegistered('yt-live-chat-item-list-renderer', (cProto) => {
  462.  
  463. const mclp = cProto;
  464. console.assert(typeof mclp.scrollToBottom_ === 'function')
  465. console.assert(typeof mclp.scrollToBottom66_ !== 'function')
  466. console.assert(typeof mclp.flushActiveItems_ === 'function')
  467. console.assert(typeof mclp.flushActiveItems66_ !== 'function')
  468.  
  469.  
  470. mclp.__intermediate_delay__ = null;
  471.  
  472. mclp.scrollToBottom66_ = mclp.scrollToBottom_;
  473. mclp.scrollToBottom_ = function () {
  474. const cnt = this;
  475. const itemScroller = cnt.itemScroller;
  476. if (scrollWillChangeController && scrollWillChangeController.element !== itemScroller) {
  477. scrollWillChangeController.release();
  478. scrollWillChangeController = null;
  479. }
  480. if (!scrollWillChangeController) scrollWillChangeController = new WillChangeController(itemScroller, 'scroll-position');
  481. const wcController = scrollWillChangeController;
  482. wcController.beforeOper();
  483. cnt.__intermediate_delay__ = new Promise(resolve => {
  484. Promise.resolve().then(() => {
  485. cnt.scrollToBottom66_();
  486. }).then(() => {
  487. wcController.afterOper();
  488. resolve();
  489. });
  490. });
  491. }
  492.  
  493. mclp.flushActiveItems66_ = mclp.flushActiveItems_;
  494. let lastFlushActiveItemsCalled = 0;
  495. mclp.flushActiveItems_ = function () {
  496. const cnt = this;
  497.  
  498. if (arguments.length !== 0) return cnt.flushActiveItems66_.apply(this, arguments);
  499.  
  500. if (cnt.activeItems_.length === 0) {
  501. cnt.__intermediate_delay__ = null;
  502. return;
  503. }
  504.  
  505. if (lastFlushActiveItemsCalled > 1e9) lastFlushActiveItemsCalled = 9;
  506. let tid = ++lastFlushActiveItemsCalled;
  507.  
  508. const items = (cnt.$ || 0).items;
  509. if (contensWillChangeController && contensWillChangeController.element !== items) {
  510. contensWillChangeController.release();
  511. contensWillChangeController = null;
  512. }
  513. if (!contensWillChangeController) contensWillChangeController = new WillChangeController(items, 'contents');
  514. const wcController = contensWillChangeController;
  515.  
  516. // ignore previous __intermediate_delay__ and create a new one
  517. cnt.__intermediate_delay__ = new Promise(resolve => {
  518. if (tid !== lastFlushActiveItemsCalled) {
  519. resolve();
  520. return;
  521. }
  522. if (cnt.activeItems_.length === 0) {
  523. resolve();
  524. } else {
  525. if (cnt.canScrollToBottom_()) {
  526. wcController.beforeOper();
  527. new Promise(requestAnimationFrame).then(() => {
  528. if (tid === lastFlushActiveItemsCalled) {
  529. const len1 = cnt.activeItems_.length;
  530. cnt.flushActiveItems66_();
  531. const len2 = cnt.activeItems_.length;
  532. return len1 !== len2;
  533. }
  534. }).then(bAsync => {
  535. if (bAsync) {
  536. cnt.async(() => {
  537. wcController.afterOper();
  538. });
  539. } else {
  540. wcController.afterOper();
  541. }
  542. resolve();
  543. })
  544. } else {
  545. Promise.resolve().then(() => {
  546. if (tid === lastFlushActiveItemsCalled) {
  547. cnt.flushActiveItems66_();
  548. }
  549. resolve();
  550. })
  551. }
  552. }
  553. });
  554.  
  555. }
  556.  
  557. mclp.async66 = mclp.async;
  558. mclp.async = function () {
  559. // ensure the previous operation is done
  560. // .async is usually after the time consuming functions like flushActiveItems_ and scrollToBottom_
  561.  
  562. (this.__intermediate_delay__ || Promise.resolve()).then(() => {
  563. this.async66.apply(this, arguments);
  564. });
  565.  
  566. }
  567.  
  568. })
  569.  
  570. });
  571.  
  572. const getProto = (element) => {
  573. let proto = null;
  574. if (element) {
  575. if (element.inst) proto = element.inst.constructor.prototype;
  576. else proto = element.constructor.prototype;
  577. }
  578. return proto || null;
  579. }
  580.  
  581. let done = 0;
  582. let main = async (q) => {
  583.  
  584. if (done) return;
  585.  
  586. if (!q) return;
  587. let m1 = nodeParent(q);
  588. let m2 = q;
  589. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  590.  
  591. done = 1;
  592.  
  593. Promise.resolve().then(watchUserCSS);
  594.  
  595. addCss();
  596.  
  597. setupStyle(m1, m2);
  598.  
  599. let lcRendererWR = null;
  600.  
  601. const lcRendererElm = () => {
  602. let lcRenderer = kRef(lcRendererWR);
  603. if (!lcRenderer || !lcRenderer.isConnected) {
  604. lcRenderer = document.querySelector('yt-live-chat-item-list-renderer.yt-live-chat-renderer');
  605. lcRendererWR = lcRenderer ? mWeakRef(lcRenderer) : null;
  606. }
  607. return lcRenderer
  608. };
  609.  
  610. let hasFirstShowMore = false;
  611.  
  612. const visObserverFn = (entry) => {
  613.  
  614. const target = entry.target;
  615. if (!target) return;
  616. let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
  617. const h = entry.boundingClientRect.height;
  618. if (h < 16) { // wrong: 8 (padding/margin); standard: 32; test: 16 or 20
  619. // e.g. under fullscreen. the element created but not rendered.
  620. target.setAttribute('wSr93', '');
  621. return;
  622. }
  623. if (isVisible) {
  624. target.style.setProperty('--wsr94', h + 'px');
  625. target.setAttribute('wSr93', 'visible');
  626. if (nNextElem(target) === null) {
  627. // firstVisibleItemDetected = true;
  628. /*
  629. if (dateNow() - lastScroll < 80) {
  630. lastLShow = 0;
  631. lastScroll = 0;
  632. Promise.resolve().then(clickShowMore);
  633. } else {
  634. lastLShow = dateNow();
  635. }
  636. */
  637. // lastLShow = dateNow();
  638. } else if (!hasFirstShowMore) { // should more than one item being visible
  639. // implement inside visObserver to ensure there is sufficient delay
  640. hasFirstShowMore = true;
  641. requestAnimationFrame(() => {
  642. // foreground page
  643. // activeDeferredAppendChild = true;
  644. // page visibly ready -> load the latest comments at initial loading
  645. const lcRenderer = lcRendererElm();
  646. if (lcRenderer) {
  647. (lcRenderer.inst || lcRenderer).scrollToBottom_();
  648. }
  649. });
  650. }
  651. }
  652. else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing
  653.  
  654. target.style.setProperty('--wsr94', h + 'px');
  655. target.setAttribute('wSr93', 'hidden');
  656. } // note: might consider 0 < entry.intersectionRatio < 0.5 and target.getAttribute('wSr93') === '' <new last item>
  657.  
  658. }
  659.  
  660. const visObserver = new IntersectionObserver((entries) => {
  661.  
  662. for (const entry of entries) {
  663.  
  664. Promise.resolve(entry).then(visObserverFn);
  665.  
  666. }
  667.  
  668. }, {
  669. /*
  670. root: items,
  671. rootMargin: "0px",
  672. threshold: 1.0,
  673. */
  674. // root: HTMLElement.prototype.closest.call(m2, '#item-scroller.yt-live-chat-item-list-renderer'), // nullable
  675. rootMargin: "0px",
  676. threshold: [0.05, 0.95],
  677. });
  678.  
  679. //m2.style.visibility='';
  680.  
  681. const mutFn = (items) => {
  682. for (let node = nLastElem(items); node !== null; node = nPrevElem(node)) {
  683. if (node.hasAttribute('wSr93')) break;
  684. node.setAttribute('wSr93', '');
  685. visObserver.observe(node);
  686. }
  687. }
  688.  
  689. const mutObserver = new MutationObserver((mutations) => {
  690. const items = (mutations[0] || 0).target;
  691. if (!items) return;
  692. mutFn(items);
  693. });
  694.  
  695. const setupMutObserver = (m2) => {
  696. mutObserver.disconnect();
  697. mutObserver.takeRecords();
  698. if (m2) {
  699. mutObserver.observe(m2, {
  700. childList: true,
  701. subtree: false
  702. });
  703. mutFn(m2);
  704. }
  705. }
  706.  
  707. setupMutObserver(m2);
  708.  
  709. const mclp = getProto(document.querySelector('yt-live-chat-item-list-renderer'));
  710. if (mclp && mclp.attached) {
  711.  
  712. mclp.attached66 = mclp.attached;
  713. mclp.attached = function () {
  714. let m2 = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer');
  715. let m1 = nodeParent(m2);
  716. setupStyle(m1, m2);
  717. setupMutObserver(m2);
  718. return this.attached66();
  719. }
  720.  
  721. mclp.detached66 = mclp.detached;
  722. mclp.detached = function () {
  723. setupMutObserver();
  724. return this.detached66();
  725. }
  726.  
  727. mclp.canScrollToBottom_ = function () {
  728. return this.atBottom && this.allowScroll && !(dateNow() - lastWheel < 80)
  729. }
  730.  
  731. mclp.isSmoothScrollEnabled_ = function () {
  732. return false;
  733. }
  734.  
  735. } else {
  736. console.warn(`proto.attached for yt-live-chat-item-list-renderer is unavailable.`)
  737. }
  738.  
  739.  
  740. let scrollCount = 0;
  741. document.addEventListener('scroll', (evt) => {
  742. if (!evt || !evt.isTrusted) return;
  743. // lastScroll = dateNow();
  744. if (++scrollCount > 1e9) scrollCount = 9;
  745. }, { passive: true, capture: true }) // support contain => support passive
  746.  
  747. // document.addEventListener('scroll', (evt) => {
  748.  
  749. // if (!evt || !evt.isTrusted) return;
  750. // if (!firstVisibleItemDetected) return;
  751. // const isUserAction = dateNow() - lastWheel < 80; // continuous wheel -> continuous scroll -> continuous wheel -> continuous scroll
  752. // if (!isUserAction) return;
  753. // // lastScroll = dateNow();
  754.  
  755. // }, { passive: true, capture: true }) // support contain => support passive
  756.  
  757.  
  758. let lastScrollCount = -1;
  759. document.addEventListener('wheel', (evt) => {
  760.  
  761. if (!evt || !evt.isTrusted) return;
  762. if (lastScrollCount === scrollCount) return;
  763. lastScrollCount = scrollCount;
  764. lastWheel = dateNow();
  765.  
  766. }, { passive: true, capture: true }) // support contain => support passive
  767.  
  768.  
  769. const fp = (renderer) => {
  770. const cnt = renderer.inst || renderer;
  771. const container = (cnt.$ || 0).container;
  772. if (container) {
  773. container.setAttribute = tickerContainerSetAttribute;
  774. }
  775. }
  776. const tags = ["yt-live-chat-ticker-paid-message-item-renderer", "yt-live-chat-ticker-paid-sticker-item-renderer",
  777. "yt-live-chat-ticker-renderer", "yt-live-chat-ticker-sponsor-item-renderer"];
  778. for (const tag of tags) {
  779. const dummy = document.createElement(tag);
  780.  
  781. const cProto = getProto(dummy);
  782. if (!cProto || !cProto.attached) {
  783. console.warn(`proto.attached for ${tag} is unavailable.`)
  784. continue;
  785. }
  786.  
  787. const __updateTimeout__ = cProto.updateTimeout;
  788.  
  789. const canDoUpdateTimeoutReplacement = (() => {
  790.  
  791. if (dummy.countdownMs < 1 && dummy.lastCountdownTimeMs < 1 && dummy.countdownMs < 1 && dummy.countdownDurationMs < 1) {
  792. return typeof dummy.setContainerWidth === 'function' && typeof dummy.slideDown === 'function';
  793. }
  794. return false;
  795.  
  796. })(dummy.inst || dummy) && ((__updateTimeout__ + "").indexOf("window.requestAnimationFrame(this.updateTimeout.bind(this))") > 0);
  797.  
  798.  
  799.  
  800. if (canDoUpdateTimeoutReplacement) {
  801.  
  802. const killTicker = (cnt) => {
  803. if ("auto" === cnt.hostElement.style.width) cnt.setContainerWidth();
  804. cnt.slideDown()
  805. };
  806.  
  807. cProto.__ratio__ = null;
  808. cProto._updateTimeout21_ = function (a) {
  809.  
  810. /*
  811. let pRatio = this.countdownMs / this.countdownDurationMs;
  812. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  813. let noMoreCountDown = this.countdownMs < 1e-6;
  814. let qRatio = this.countdownMs / this.countdownDurationMs;
  815. if(noMoreCountDown){
  816. this.countdownMs = 0;
  817. this.ratio = 0;
  818. } else if( pRatio - qRatio < 0.001 && qRatio < pRatio){
  819.  
  820. }else{
  821. this.ratio = qRatio;
  822. }
  823. */
  824.  
  825. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  826.  
  827. let currentRatio = this.__ratio__;
  828. let tdv = this.countdownMs / this.countdownDurationMs;
  829. let nextRatio = Math.round(tdv * 500) / 500; // might generate 0.143000000001
  830.  
  831. const validCountDown = nextRatio > 0;
  832. const isAttached = this.isAttached;
  833.  
  834. if (!validCountDown) {
  835.  
  836. this.lastCountdownTimeMs = null;
  837.  
  838. this.countdownMs = 0;
  839. this.__ratio__ = null;
  840. this.ratio = 0;
  841.  
  842. if (isAttached) Promise.resolve(this).then(killTicker);
  843.  
  844. } else if (!isAttached) {
  845.  
  846. this.lastCountdownTimeMs = null;
  847.  
  848. } else {
  849.  
  850. this.lastCountdownTimeMs = a;
  851.  
  852. const ratioDiff = currentRatio - nextRatio; // 0.144 - 0.142 = 0.002
  853. if (ratioDiff < 0.001 && ratioDiff > -1e-6) {
  854. // ratioDiff = 0
  855.  
  856. } else {
  857. // ratioDiff = 0.002 / 0.004 ....
  858. // OR ratioDiff < 0
  859.  
  860. this.__ratio__ = nextRatio;
  861.  
  862. this.ratio = nextRatio;
  863. }
  864.  
  865. return true;
  866. }
  867.  
  868. };
  869.  
  870. cProto._updateTimeout21_ = function (a) {
  871. this.countdownMs = Math.max(0, this.countdownMs - (a - (this.lastCountdownTimeMs || 0)));
  872. this.ratio = this.countdownMs / this.countdownDurationMs;
  873. if (this.isAttached && this.countdownMs) {
  874. this.lastCountdownTimeMs = a;
  875. return true;
  876. } else {
  877. this.lastCountdownTimeMs = null;
  878. if (this.isAttached) {
  879. ("auto" === this.hostElement.style.width && this.setContainerWidth(), this.slideDown())
  880. }
  881. }
  882. }
  883.  
  884.  
  885. // temporarily removed; buggy for playback
  886. /*
  887. cProto.updateTimeout = async function (a) {
  888. let ret = this._updateTimeout21_(a);
  889. while (ret) {
  890. let a = await new Promise(resolve => {
  891. this.rafId = requestAnimationFrame(resolve)
  892. }); // could be never resolve
  893. ret = this._updateTimeout21_(a);
  894. }
  895. };
  896. */
  897.  
  898.  
  899. }
  900.  
  901. cProto.attached77 = cProto.attached
  902.  
  903. cProto.attached = function () {
  904. fp(this.hostElement || this);
  905. return this.attached77();
  906. }
  907.  
  908. for (const elm of document.getElementsByTagName(tag)) {
  909. fp(elm);
  910. }
  911.  
  912.  
  913. }
  914.  
  915. };
  916.  
  917.  
  918. function onReady() {
  919. let tmObserver = new MutationObserver(() => {
  920.  
  921. let p = document.getElementById('items'); // fast
  922. if (!p) return;
  923. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer'); // check
  924.  
  925. if (q) {
  926. tmObserver.disconnect();
  927. tmObserver.takeRecords();
  928. tmObserver = null;
  929. Promise.resolve(q).then((q) => {
  930. // confirm Promis.resolve() is resolveable
  931. // execute main without direct blocking
  932. main(q);
  933. })
  934. }
  935.  
  936. });
  937.  
  938. tmObserver.observe(document.body || document.documentElement, {
  939. childList: true,
  940. subtree: true
  941. });
  942.  
  943. }
  944.  
  945. Promise.resolve().then(() => {
  946.  
  947. if (document.readyState !== 'loading') {
  948. onReady();
  949. } else {
  950. window.addEventListener("DOMContentLoaded", onReady, false);
  951. }
  952.  
  953. });
  954.  
  955. })({ Promise, requestAnimationFrame, IntersectionObserver });

QingJ © 2025

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