YouTube 超快聊天

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

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

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @version 0.6.1
  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. html #panel-pages.yt-live-chat-renderer > #input-panel.yt-live-chat-renderer:not(:empty) {
  162. --yt-live-chat-action-panel-top-border: none;
  163. }
  164.  
  165. html #panel-pages.yt-live-chat-renderer > #input-panel.yt-live-chat-renderer.iron-selected > *:first-child {
  166. border-top: 1px solid var(--yt-live-chat-panel-pages-border-color);
  167. }
  168.  
  169. html #panel-pages.yt-live-chat-renderer {
  170. border-top: 0;
  171. border-bottom: 0;
  172. }
  173.  
  174. #input-panel #picker-buttons yt-live-chat-icon-toggle-button-renderer#product-picker {
  175. overflow: hidden;
  176. contain: layout paint style;
  177. }
  178.  
  179. #chat.yt-live-chat-renderer ~ #panel-pages.yt-live-chat-renderer {
  180. overflow: visible;
  181. }
  182.  
  183. }
  184.  
  185. `;
  186.  
  187. const { Promise, requestAnimationFrame, IntersectionObserver } = __CONTEXT__;
  188.  
  189.  
  190. const isContainSupport = CSS.supports('contain', 'layout paint style');
  191. if (!isContainSupport) {
  192. console.warn("Your browser does not support css property 'contain'.\nPlease upgrade to the latest version.".trim());
  193. }
  194.  
  195. // const APPLY_delayAppendChild = false;
  196.  
  197. // let activeDeferredAppendChild = false; // deprecated
  198.  
  199. // let delayedAppendParentWS = new WeakSet();
  200. // let delayedAppendOperations = [];
  201. // let commonAppendParentStackSet = new Set();
  202.  
  203. // let firstVisibleItemDetected = false; // deprecated
  204.  
  205. const sp7 = Symbol();
  206.  
  207.  
  208. let dt0 = Date.now() - 2000;
  209. const dateNow = () => Date.now() - dt0;
  210. // let lastScroll = 0;
  211. // let lastLShow = 0;
  212. let lastWheel = 0;
  213.  
  214. const proxyHelperFn = (dummy) => ({
  215.  
  216. get(target, prop) {
  217. return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
  218. },
  219. set(target, prop, value) {
  220. if (!(prop in dummy)) {
  221. target[prop] = value;
  222. }
  223. return true;
  224. },
  225. has(target, prop) {
  226. return (prop in target)
  227. },
  228. deleteProperty(target, prop) {
  229. return true;
  230. },
  231. ownKeys(target) {
  232. return Object.keys(target);
  233. },
  234. defineProperty(target, key, descriptor) {
  235. return Object.defineProperty(target, key, descriptor);
  236. },
  237. getOwnPropertyDescriptor(target, key) {
  238. return Object.getOwnPropertyDescriptor(target, key);
  239. },
  240.  
  241. });
  242.  
  243. const tickerContainerSetAttribute = function (attrName, attrValue) { // ensure '14.30000001%'.toFixed(1)
  244.  
  245. let yd = (this.__dataHost || (this.inst || 0).__dataHost).__data;
  246.  
  247. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  248.  
  249. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  250. let v = `${attrValue}`;
  251. // conside a ticker is 101px width
  252. // 1% = 1.01px
  253. // 0.2% = 0.202px
  254.  
  255.  
  256. const ratio1 = (yd.ratio * 100);
  257. if (ratio1 > -1) { // avoid NaN
  258.  
  259. // countdownDurationMs
  260. // 600000 - 0.2% <1% = 6s> <0.2% = 1.2s>
  261. // 300000 - 0.5% <1% = 3s> <0.5% = 1.5s>
  262. // 150000 - 1% <1% = 1.5s>
  263. // 75000 - 2% <1% =0.75s > <2% = 1.5s>
  264. // 30000 - 5% <1% =0.3s > <5% = 1.5s>
  265.  
  266. // 99px * 5% = 4.95px
  267.  
  268. // 15000 - 10% <1% =0.15s > <10% = 1.5s>
  269.  
  270.  
  271.  
  272.  
  273. // 1% Duration
  274.  
  275. let ratio2 = ratio1;
  276.  
  277. const ydd = yd.data;
  278. const d1 = ydd.durationSec;
  279. const d2 = ydd.fullDurationSec;
  280.  
  281. if (d1 === d2 && d1 > 1) {
  282.  
  283. if (d1 > 400) ratio2 = Math.round(ratio2 * 5) / 5; // 0.2%
  284. else if (d1 > 200) ratio2 = Math.round(ratio2 * 2) / 2; // 0.5%
  285. else if (d1 > 100) ratio2 = Math.round(ratio2 * 1) / 1; // 1%
  286. else if (d1 > 50) ratio2 = Math.round(ratio2 * 0.5) / 0.5; // 2%
  287. else if (d1 > 25) ratio2 = Math.round(ratio2 * 0.2) / 0.2; // 5% (max => 99px * 5% = 4.95px)
  288. else ratio2 = Math.round(ratio2 * 0.2) / 0.2;
  289.  
  290. } else {
  291. ratio2 = Math.round(ratio2 * 5) / 5; // 0.2% (min)
  292. }
  293.  
  294. // ratio2 = Math.round(ratio2 * 5) / 5;
  295. ratio2 = ratio2.toFixed(1)
  296. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  297.  
  298. if (yd.__style_last__ === v) return;
  299. yd.__style_last__ = v;
  300. // do not consider any delay here.
  301. // it shall be inside the looping for all properties changes. all the css background ops are in the same microtask.
  302.  
  303. }
  304.  
  305. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  306.  
  307.  
  308. } else {
  309. HTMLElement.prototype.setAttribute.apply(this, arguments);
  310. }
  311.  
  312. };
  313.  
  314. const fxOperator = (proto, propertyName) => {
  315. let propertyDescriptorGetter = null;
  316. try {
  317. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  318. } catch (e) { }
  319. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  320. };
  321.  
  322. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  323. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  324. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  325. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  326. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  327.  
  328.  
  329. /* globals WeakRef:false */
  330.  
  331. /** @type {(o: Object | null) => WeakRef | null} */
  332. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  333.  
  334. /** @type {(wr: Object | null) => Object | null} */
  335. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  336.  
  337. const watchUserCSS = () => {
  338.  
  339. // if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;
  340.  
  341. const getElemFromWR = (nr) => {
  342. const n = kRef(nr);
  343. if (n && n.isConnected) return n;
  344. return null;
  345. }
  346.  
  347. const clearContentVisibilitySizing = () => {
  348. Promise.resolve().then(() => {
  349.  
  350. let btnShowMoreWR = mWeakRef(document.querySelector('#show-more[disabled]'));
  351.  
  352. let lastVisibleItemWR = null;
  353. for (const elm of document.querySelectorAll('[wSr93]')) {
  354. if (elm.getAttribute('wSr93') === 'visible') lastVisibleItemWR = mWeakRef(elm);
  355. elm.setAttribute('wSr93', '');
  356. // custom CSS property --wsr94 not working when attribute wSr93 removed
  357. }
  358. requestAnimationFrame(() => {
  359. const btnShowMore = getElemFromWR(btnShowMoreWR); btnShowMoreWR = null;
  360. if (btnShowMore) btnShowMore.click();
  361. else {
  362. // would not work if switch it frequently
  363. const lastVisibleItem = getElemFromWR(lastVisibleItemWR); lastVisibleItemWR = null;
  364. if (lastVisibleItem) {
  365.  
  366. Promise.resolve()
  367. .then(() => lastVisibleItem.scrollIntoView())
  368. .then(() => lastVisibleItem.scrollIntoView(false))
  369. .then(() => lastVisibleItem.scrollIntoView({ behavior: "instant", block: "end", inline: "nearest" }))
  370. .catch(e => { }) // break the chain when method not callable
  371.  
  372. }
  373. }
  374. })
  375.  
  376. })
  377.  
  378. }
  379.  
  380. const mutObserver = new MutationObserver((mutations) => {
  381. for (const mutation of mutations) {
  382. if ((mutation.addedNodes || 0).length >= 1) {
  383. for (const addedNode of mutation.addedNodes) {
  384. if (addedNode.nodeName === 'STYLE') {
  385. clearContentVisibilitySizing();
  386. return;
  387. }
  388. }
  389. }
  390. if ((mutation.removedNodes || 0).length >= 1) {
  391. for (const removedNode of mutation.removedNodes) {
  392. if (removedNode.nodeName === 'STYLE') {
  393. clearContentVisibilitySizing();
  394. return;
  395. }
  396. }
  397. }
  398. }
  399. });
  400.  
  401. mutObserver.observe(document.documentElement, {
  402. childList: true,
  403. subtree: false
  404. })
  405.  
  406. mutObserver.observe(document.head, {
  407. childList: true,
  408. subtree: false
  409. })
  410. mutObserver.observe(document.body, {
  411. childList: true,
  412. subtree: false
  413. });
  414.  
  415. }
  416.  
  417. const setupStyle = (m1, m2) => {
  418.  
  419. const dummy1v = {
  420. transform: '',
  421. height: '',
  422. minHeight: '',
  423. paddingBottom: '',
  424. paddingTop: ''
  425. };
  426. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  427. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  428. }
  429.  
  430. const dummy1p = proxyHelperFn(dummy1v);
  431. const sp1v = new Proxy(m1.style, dummy1p);
  432. const sp2v = new Proxy(m2.style, dummy1p);
  433. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  434. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  435. m1.removeAttribute("style");
  436. m2.removeAttribute("style");
  437.  
  438. }
  439.  
  440.  
  441. class WillChangeController {
  442. constructor(itemScroller, willChangeValue) {
  443. this.element = itemScroller;
  444. this.counter = 0;
  445. this.active = false;
  446. this.willChangeValue = willChangeValue;
  447. }
  448.  
  449. beforeOper() {
  450. if (!this.active) {
  451. this.active = true;
  452. this.element.style.willChange = this.willChangeValue;
  453. }
  454. this.counter++;
  455. }
  456.  
  457. afterOper() {
  458. const c = this.counter;
  459. requestAnimationFrame(() => {
  460. if (c === this.counter) {
  461. this.active = false;
  462. this.element.style.willChange = '';
  463. }
  464. })
  465. }
  466.  
  467. release() {
  468. const element = this.element;
  469. this.element = null;
  470. this.counter = 1e16;
  471. this.active = false;
  472. try {
  473. element.style.willChange = '';
  474. } catch (e) { }
  475. }
  476.  
  477. }
  478.  
  479.  
  480. customYtElements.onRegistryReady(() => {
  481.  
  482. let scrollWillChangeController = null;
  483. let contensWillChangeController = null;
  484.  
  485. // as it links to event handling, it has to be injected using immediateCallback
  486. customYtElements.whenRegistered('yt-live-chat-item-list-renderer', (cProto) => {
  487.  
  488. const mclp = cProto;
  489. console.assert(typeof mclp.scrollToBottom_ === 'function')
  490. console.assert(typeof mclp.scrollToBottom66_ !== 'function')
  491. console.assert(typeof mclp.flushActiveItems_ === 'function')
  492. console.assert(typeof mclp.flushActiveItems66_ !== 'function')
  493. console.assert(typeof mclp.async === 'function')
  494.  
  495.  
  496. mclp.__intermediate_delay__ = null;
  497.  
  498. let mzk = 0;
  499. let myk = 0;
  500. let mlf = 0;
  501. let myw = 0;
  502. let mzt = 0;
  503. let zarr = null;
  504.  
  505. if ((mclp.clearList || 0).length === 0) {
  506. mclp.clearList66 = mclp.clearList;
  507. mclp.clearList = function () {
  508. mzk++;
  509. myk++;
  510. mlf++;
  511. myw++;
  512. mzt++;
  513. zarr = null;
  514. this.__intermediate_delay__ = null;
  515. this.clearList66();
  516. };
  517. }
  518.  
  519. if ((mclp.scrollToBottom_ || 0).length === 0) {
  520.  
  521. mclp.scrollToBottom66_ = mclp.scrollToBottom_;
  522.  
  523. mclp.scrollToBottom77_ = async function () {
  524. if (mzk > 1e9) mzk = 9;
  525. let tid = ++mzk;
  526.  
  527. await new Promise(requestAnimationFrame);
  528.  
  529. if (tid !== mzk) {
  530. return;
  531. }
  532.  
  533. const cnt = this;
  534. const itemScroller = cnt.itemScroller;
  535. if (scrollWillChangeController && scrollWillChangeController.element !== itemScroller) {
  536. scrollWillChangeController.release();
  537. scrollWillChangeController = null;
  538. }
  539. if (!scrollWillChangeController) scrollWillChangeController = new WillChangeController(itemScroller, 'scroll-position');
  540. const wcController = scrollWillChangeController;
  541. wcController.beforeOper();
  542.  
  543. await Promise.resolve();
  544. cnt.scrollToBottom66_();
  545.  
  546. await Promise.resolve();
  547. wcController.afterOper();
  548.  
  549. }
  550.  
  551. mclp.scrollToBottom_ = function () {
  552. const cnt = this;
  553. cnt.__intermediate_delay__ = new Promise(resolve => {
  554. cnt.scrollToBottom77_().then(() => {
  555. resolve();
  556. });
  557. });
  558. }
  559. }
  560.  
  561.  
  562. if ((mclp.showNewItems_ || 0).length === 0) {
  563.  
  564.  
  565. mclp.showNewItems66_ = mclp.showNewItems_;
  566. mclp.showNewItems77_ = async function () {
  567. if (myk > 1e9) myk = 9;
  568. let tid = ++myk;
  569.  
  570. await new Promise(requestAnimationFrame);
  571.  
  572. if (tid !== myk) {
  573. return;
  574. }
  575.  
  576. const cnt = this;
  577.  
  578. await Promise.resolve();
  579. cnt.showNewItems66_();
  580.  
  581. await Promise.resolve();
  582.  
  583. }
  584.  
  585. mclp.showNewItems_ = function () {
  586.  
  587. const cnt = this;
  588. cnt.__intermediate_delay__ = new Promise(resolve => {
  589. cnt.showNewItems77_().then(() => {
  590. resolve();
  591. });
  592. });
  593. }
  594.  
  595. }
  596.  
  597.  
  598.  
  599.  
  600. if ((mclp.flushActiveItems_ || 0).length === 0) {
  601.  
  602.  
  603. mclp.flushActiveItems66_ = mclp.flushActiveItems_;
  604.  
  605.  
  606. mclp.flushActiveItems77_ = async function () {
  607. try {
  608.  
  609. const cnt = this;
  610. if (mlf > 1e9) mlf = 9;
  611. let tid = ++mlf;
  612. if (tid !== mlf || cnt.isAttached === false || (cnt.hostElement || cnt).isConnected === false) return;
  613. if (!cnt.activeItems_ || cnt.activeItems_.length === 0) return;
  614. if (cnt.canScrollToBottom_()) {
  615. let immd = cnt.__intermediate_delay__;
  616. await new Promise(requestAnimationFrame);
  617. if (tid !== mlf || cnt.isAttached === false || (cnt.hostElement || cnt).isConnected === false) return;
  618. if (!cnt.activeItems_ || cnt.activeItems_.length === 0) return;
  619.  
  620. const items = (cnt.$ || 0).items;
  621. if (contensWillChangeController && contensWillChangeController.element !== items) {
  622. contensWillChangeController.release();
  623. contensWillChangeController = null;
  624. }
  625. if (!contensWillChangeController) contensWillChangeController = new WillChangeController(items, 'contents');
  626. const wcController = contensWillChangeController;
  627. cnt.__intermediate_delay__ = Promise.all([cnt.__intermediate_delay__ || null, immd || null]);
  628. wcController.beforeOper();
  629. await Promise.resolve();
  630. const len1 = cnt.activeItems_.length;
  631. cnt.flushActiveItems66_();
  632. const len2 = cnt.activeItems_.length;
  633. let bAsync = len1 !== len2;
  634. await Promise.resolve();
  635. if (bAsync) {
  636. cnt.async(() => {
  637. wcController.afterOper();
  638. });
  639. } else {
  640. wcController.afterOper();
  641. }
  642. return 1;
  643. } else {
  644. cnt.flushActiveItems66_();
  645. return 2;
  646. }
  647. } catch (e) {
  648. console.warn(e);
  649. }
  650. }
  651.  
  652. mclp.flushActiveItems_ = function () {
  653. const cnt = this;
  654.  
  655. if (arguments.length !== 0 || !cnt.activeItems_ || !cnt.canScrollToBottom_) return cnt.flushActiveItems66_.apply(this, arguments);
  656.  
  657. if (cnt.activeItems_.length === 0) {
  658. cnt.__intermediate_delay__ = null;
  659. return;
  660. }
  661.  
  662. const cntData = ((cnt || 0).data || 0);
  663. if (cntData.maxItemsToDisplay > 90) cntData.maxItemsToDisplay = 90;
  664.  
  665. // ignore previous __intermediate_delay__ and create a new one
  666. cnt.__intermediate_delay__ = new Promise(resolve => {
  667. cnt.flushActiveItems77_().then(rt => {
  668. if (rt === 1) resolve(1); // success, scroll to bottom
  669. else if (rt === 2) resolve(2); // success, trim
  670. else resolve(-1); // skip
  671. });
  672. });
  673.  
  674. }
  675. }
  676.  
  677. if ((mclp.async || 0).length === 2) {
  678.  
  679.  
  680. mclp.async66 = mclp.async;
  681. mclp.async = function () {
  682. // ensure the previous operation is done
  683. // .async is usually after the time consuming functions like flushActiveItems_ and scrollToBottom_
  684.  
  685. const stack = new Error().stack;
  686. const isFlushAsync = stack.indexOf('flushActiveItems_') >= 0;
  687. (this.__intermediate_delay__ || Promise.resolve()).then(rk => {
  688. if (isFlushAsync) {
  689. if (rk < 0) return;
  690. if (rk === 2 && arguments[0] === this.maybeScrollToBottom_) return;
  691. }
  692. this.async66.apply(this, arguments);
  693. });
  694.  
  695. }
  696.  
  697. }
  698.  
  699.  
  700. if ((mclp.onScrollItems_ || 0).length === 1) {
  701.  
  702. mclp.onScrollItems66_ = mclp.onScrollItems_;
  703. mclp.onScrollItems77_ = async function (evt) {
  704. if (myw > 1e9) myw = 9;
  705. let tid = ++myw;
  706.  
  707. await new Promise(requestAnimationFrame);
  708.  
  709. if (tid !== myw) {
  710. return;
  711. }
  712.  
  713. const cnt = this;
  714.  
  715. await Promise.resolve();
  716. cnt.onScrollItems66_(evt);
  717.  
  718. await Promise.resolve();
  719.  
  720. }
  721.  
  722. mclp.onScrollItems_ = function (evt) {
  723.  
  724. const cnt = this;
  725. cnt.__intermediate_delay__ = new Promise(resolve => {
  726. cnt.onScrollItems77_(evt).then(() => {
  727. resolve();
  728. });
  729. });
  730. }
  731. }
  732.  
  733. if ((mclp.handleLiveChatActions_ || 0).length === 1) {
  734. mclp.handleLiveChatActions66_ = mclp.handleLiveChatActions_;
  735.  
  736. mclp.handleLiveChatActions77_ = async function (arr) {
  737. if (typeof (arr || 0).length !== 'number') {
  738. this.handleLiveChatActions66_(arr);
  739. return;
  740. }
  741. if (mzt > 1e9) mzt = 9;
  742. let tid = ++mzt;
  743.  
  744. if (zarr === null) zarr = arr;
  745. else Array.prototype.push.apply(zarr, arr);
  746. arr = null;
  747.  
  748. await new Promise(requestAnimationFrame);
  749.  
  750. if (tid !== mzt || zarr === null) {
  751. return;
  752. }
  753.  
  754. const carr = zarr;
  755. zarr = null;
  756.  
  757. await Promise.resolve();
  758. this.handleLiveChatActions66_(carr);
  759. await Promise.resolve();
  760.  
  761. }
  762.  
  763. mclp.handleLiveChatActions_ = function (arr) {
  764.  
  765. const cnt = this;
  766. cnt.__intermediate_delay__ = new Promise(resolve => {
  767. cnt.handleLiveChatActions77_(arr).then(() => {
  768. resolve();
  769. });
  770. });
  771. }
  772.  
  773. }
  774.  
  775.  
  776.  
  777.  
  778. })
  779.  
  780. });
  781.  
  782. const getProto = (element) => {
  783. let proto = null;
  784. if (element) {
  785. if (element.inst) proto = element.inst.constructor.prototype;
  786. else proto = element.constructor.prototype;
  787. }
  788. return proto || null;
  789. }
  790.  
  791. let done = 0;
  792. let main = async (q) => {
  793.  
  794. if (done) return;
  795.  
  796. if (!q) return;
  797. let m1 = nodeParent(q);
  798. let m2 = q;
  799. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  800.  
  801. done = 1;
  802.  
  803. Promise.resolve().then(watchUserCSS);
  804.  
  805. addCss();
  806.  
  807. setupStyle(m1, m2);
  808.  
  809. let lcRendererWR = null;
  810.  
  811. const lcRendererElm = () => {
  812. let lcRenderer = kRef(lcRendererWR);
  813. if (!lcRenderer || !lcRenderer.isConnected) {
  814. lcRenderer = document.querySelector('yt-live-chat-item-list-renderer.yt-live-chat-renderer');
  815. lcRendererWR = lcRenderer ? mWeakRef(lcRenderer) : null;
  816. }
  817. return lcRenderer
  818. };
  819.  
  820. let hasFirstShowMore = false;
  821.  
  822. const visObserverFn = (entry) => {
  823.  
  824. const target = entry.target;
  825. if (!target) return;
  826. let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
  827. const h = entry.boundingClientRect.height;
  828. if (h < 16) { // wrong: 8 (padding/margin); standard: 32; test: 16 or 20
  829. // e.g. under fullscreen. the element created but not rendered.
  830. target.setAttribute('wSr93', '');
  831. return;
  832. }
  833. if (isVisible) {
  834. target.style.setProperty('--wsr94', h + 'px');
  835. target.setAttribute('wSr93', 'visible');
  836. if (nNextElem(target) === null) {
  837. // firstVisibleItemDetected = true;
  838. /*
  839. if (dateNow() - lastScroll < 80) {
  840. lastLShow = 0;
  841. lastScroll = 0;
  842. Promise.resolve().then(clickShowMore);
  843. } else {
  844. lastLShow = dateNow();
  845. }
  846. */
  847. // lastLShow = dateNow();
  848. } else if (!hasFirstShowMore) { // should more than one item being visible
  849. // implement inside visObserver to ensure there is sufficient delay
  850. hasFirstShowMore = true;
  851. requestAnimationFrame(() => {
  852. // foreground page
  853. // activeDeferredAppendChild = true;
  854. // page visibly ready -> load the latest comments at initial loading
  855. const lcRenderer = lcRendererElm();
  856. if (lcRenderer) {
  857. (lcRenderer.inst || lcRenderer).scrollToBottom_();
  858. }
  859. });
  860. }
  861. }
  862. else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing
  863.  
  864. target.style.setProperty('--wsr94', h + 'px');
  865. target.setAttribute('wSr93', 'hidden');
  866. } // note: might consider 0 < entry.intersectionRatio < 0.5 and target.getAttribute('wSr93') === '' <new last item>
  867.  
  868. }
  869.  
  870. const visObserver = new IntersectionObserver((entries) => {
  871.  
  872. for (const entry of entries) {
  873.  
  874. Promise.resolve(entry).then(visObserverFn);
  875.  
  876. }
  877.  
  878. }, {
  879. /*
  880. root: items,
  881. rootMargin: "0px",
  882. threshold: 1.0,
  883. */
  884. // root: HTMLElement.prototype.closest.call(m2, '#item-scroller.yt-live-chat-item-list-renderer'), // nullable
  885. rootMargin: "0px",
  886. threshold: [0.05, 0.95],
  887. });
  888.  
  889. //m2.style.visibility='';
  890.  
  891. const mutFn = (items) => {
  892. for (let node = nLastElem(items); node !== null; node = nPrevElem(node)) {
  893. if (node.hasAttribute('wSr93')) break;
  894. node.setAttribute('wSr93', '');
  895. visObserver.observe(node);
  896. }
  897. }
  898.  
  899. const mutObserver = new MutationObserver((mutations) => {
  900. const items = (mutations[0] || 0).target;
  901. if (!items) return;
  902. mutFn(items);
  903. });
  904.  
  905. const setupMutObserver = (m2) => {
  906. mutObserver.disconnect();
  907. mutObserver.takeRecords();
  908. if (m2) {
  909. mutObserver.observe(m2, {
  910. childList: true,
  911. subtree: false
  912. });
  913. mutFn(m2);
  914. }
  915. }
  916.  
  917. setupMutObserver(m2);
  918.  
  919. const mclp = getProto(document.querySelector('yt-live-chat-item-list-renderer'));
  920. if (mclp && mclp.attached) {
  921.  
  922. mclp.attached66 = mclp.attached;
  923. mclp.attached = function () {
  924. let m2 = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer');
  925. let m1 = nodeParent(m2);
  926. setupStyle(m1, m2);
  927. setupMutObserver(m2);
  928. return this.attached66();
  929. }
  930.  
  931. mclp.detached66 = mclp.detached;
  932. mclp.detached = function () {
  933. setupMutObserver();
  934. return this.detached66();
  935. }
  936.  
  937. mclp.canScrollToBottom_ = function () {
  938. return this.atBottom && this.allowScroll && !(dateNow() - lastWheel < 80)
  939. }
  940.  
  941. mclp.isSmoothScrollEnabled_ = function () {
  942. return false;
  943. }
  944.  
  945. mclp.maybeResizeScrollContainer_ = function () {
  946. //
  947. }
  948.  
  949. mclp.refreshOffsetContainerHeight_ = function () {
  950. //
  951. }
  952.  
  953. mclp.smoothScroll_ = function () {
  954. //
  955. }
  956.  
  957. mclp.resetSmoothScroll_ = function () {
  958. //
  959. }
  960.  
  961. } else {
  962. console.warn(`proto.attached for yt-live-chat-item-list-renderer is unavailable.`)
  963. }
  964.  
  965.  
  966. let scrollCount = 0;
  967. document.addEventListener('scroll', (evt) => {
  968. if (!evt || !evt.isTrusted) return;
  969. // lastScroll = dateNow();
  970. if (++scrollCount > 1e9) scrollCount = 9;
  971. }, { passive: true, capture: true }) // support contain => support passive
  972.  
  973. // document.addEventListener('scroll', (evt) => {
  974.  
  975. // if (!evt || !evt.isTrusted) return;
  976. // if (!firstVisibleItemDetected) return;
  977. // const isUserAction = dateNow() - lastWheel < 80; // continuous wheel -> continuous scroll -> continuous wheel -> continuous scroll
  978. // if (!isUserAction) return;
  979. // // lastScroll = dateNow();
  980.  
  981. // }, { passive: true, capture: true }) // support contain => support passive
  982.  
  983.  
  984. let lastScrollCount = -1;
  985. document.addEventListener('wheel', (evt) => {
  986.  
  987. if (!evt || !evt.isTrusted) return;
  988. if (lastScrollCount === scrollCount) return;
  989. lastScrollCount = scrollCount;
  990. lastWheel = dateNow();
  991.  
  992. }, { passive: true, capture: true }) // support contain => support passive
  993.  
  994.  
  995. const fp = (renderer) => {
  996. const cnt = renderer.inst || renderer;
  997. const container = (cnt.$ || 0).container;
  998. if (container) {
  999. container.setAttribute = tickerContainerSetAttribute;
  1000. }
  1001. }
  1002. const tags = ["yt-live-chat-ticker-paid-message-item-renderer", "yt-live-chat-ticker-paid-sticker-item-renderer",
  1003. "yt-live-chat-ticker-renderer", "yt-live-chat-ticker-sponsor-item-renderer"];
  1004. for (const tag of tags) {
  1005. const dummy = document.createElement(tag);
  1006.  
  1007. const cProto = getProto(dummy);
  1008. if (!cProto || !cProto.attached) {
  1009. console.warn(`proto.attached for ${tag} is unavailable.`)
  1010. continue;
  1011. }
  1012.  
  1013. const __updateTimeout__ = cProto.updateTimeout;
  1014.  
  1015. const canDoUpdateTimeoutReplacement = (() => {
  1016.  
  1017. if (dummy.countdownMs < 1 && dummy.lastCountdownTimeMs < 1 && dummy.countdownMs < 1 && dummy.countdownDurationMs < 1) {
  1018. return typeof dummy.setContainerWidth === 'function' && typeof dummy.slideDown === 'function';
  1019. }
  1020. return false;
  1021.  
  1022. })(dummy.inst || dummy) && ((__updateTimeout__ + "").indexOf("window.requestAnimationFrame(this.updateTimeout.bind(this))") > 0);
  1023.  
  1024.  
  1025.  
  1026. if (canDoUpdateTimeoutReplacement) {
  1027.  
  1028. const killTicker = (cnt) => {
  1029. if ("auto" === cnt.hostElement.style.width) cnt.setContainerWidth();
  1030. cnt.slideDown()
  1031. };
  1032.  
  1033. cProto.__ratio__ = null;
  1034. cProto._updateTimeout21_ = function (a) {
  1035.  
  1036. /*
  1037. let pRatio = this.countdownMs / this.countdownDurationMs;
  1038. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  1039. let noMoreCountDown = this.countdownMs < 1e-6;
  1040. let qRatio = this.countdownMs / this.countdownDurationMs;
  1041. if(noMoreCountDown){
  1042. this.countdownMs = 0;
  1043. this.ratio = 0;
  1044. } else if( pRatio - qRatio < 0.001 && qRatio < pRatio){
  1045.  
  1046. }else{
  1047. this.ratio = qRatio;
  1048. }
  1049. */
  1050.  
  1051. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  1052.  
  1053. let currentRatio = this.__ratio__;
  1054. let tdv = this.countdownMs / this.countdownDurationMs;
  1055. let nextRatio = Math.round(tdv * 500) / 500; // might generate 0.143000000001
  1056.  
  1057. const validCountDown = nextRatio > 0;
  1058. const isAttached = this.isAttached;
  1059.  
  1060. if (!validCountDown) {
  1061.  
  1062. this.lastCountdownTimeMs = null;
  1063.  
  1064. this.countdownMs = 0;
  1065. this.__ratio__ = null;
  1066. this.ratio = 0;
  1067.  
  1068. if (isAttached) Promise.resolve(this).then(killTicker);
  1069.  
  1070. } else if (!isAttached) {
  1071.  
  1072. this.lastCountdownTimeMs = null;
  1073.  
  1074. } else {
  1075.  
  1076. this.lastCountdownTimeMs = a;
  1077.  
  1078. const ratioDiff = currentRatio - nextRatio; // 0.144 - 0.142 = 0.002
  1079. if (ratioDiff < 0.001 && ratioDiff > -1e-6) {
  1080. // ratioDiff = 0
  1081.  
  1082. } else {
  1083. // ratioDiff = 0.002 / 0.004 ....
  1084. // OR ratioDiff < 0
  1085.  
  1086. this.__ratio__ = nextRatio;
  1087.  
  1088. this.ratio = nextRatio;
  1089. }
  1090.  
  1091. return true;
  1092. }
  1093.  
  1094. };
  1095.  
  1096. cProto._updateTimeout21_ = function (a) {
  1097. this.countdownMs = Math.max(0, this.countdownMs - (a - (this.lastCountdownTimeMs || 0)));
  1098. this.ratio = this.countdownMs / this.countdownDurationMs;
  1099. if (this.isAttached && this.countdownMs) {
  1100. this.lastCountdownTimeMs = a;
  1101. return true;
  1102. } else {
  1103. this.lastCountdownTimeMs = null;
  1104. if (this.isAttached) {
  1105. ("auto" === this.hostElement.style.width && this.setContainerWidth(), this.slideDown())
  1106. }
  1107. }
  1108. }
  1109.  
  1110.  
  1111. // temporarily removed; buggy for playback
  1112. /*
  1113. cProto.updateTimeout = async function (a) {
  1114.  
  1115. let ret = this._updateTimeout21_(a);
  1116. while (ret) {
  1117. let a = await new Promise(resolve => {
  1118. this.rafId = requestAnimationFrame(resolve)
  1119. }); // could be never resolve
  1120. ret = this._updateTimeout21_(a);
  1121. }
  1122.  
  1123. };
  1124. */
  1125.  
  1126.  
  1127. }
  1128.  
  1129. cProto.attached77 = cProto.attached
  1130.  
  1131. cProto.attached = function () {
  1132. fp(this.hostElement || this);
  1133. return this.attached77();
  1134. }
  1135.  
  1136. for (const elm of document.getElementsByTagName(tag)) {
  1137. fp(elm);
  1138. }
  1139.  
  1140.  
  1141. }
  1142.  
  1143. };
  1144.  
  1145.  
  1146. function onReady() {
  1147. let tmObserver = new MutationObserver(() => {
  1148.  
  1149. let p = document.getElementById('items'); // fast
  1150. if (!p) return;
  1151. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer'); // check
  1152.  
  1153. if (q) {
  1154. tmObserver.disconnect();
  1155. tmObserver.takeRecords();
  1156. tmObserver = null;
  1157. Promise.resolve(q).then((q) => {
  1158. // confirm Promis.resolve() is resolveable
  1159. // execute main without direct blocking
  1160. main(q);
  1161. })
  1162. }
  1163.  
  1164. });
  1165.  
  1166. tmObserver.observe(document.body || document.documentElement, {
  1167. childList: true,
  1168. subtree: true
  1169. });
  1170.  
  1171. }
  1172.  
  1173. Promise.resolve().then(() => {
  1174.  
  1175. if (document.readyState !== 'loading') {
  1176. onReady();
  1177. } else {
  1178. window.addEventListener("DOMContentLoaded", onReady, false);
  1179. }
  1180.  
  1181. });
  1182.  
  1183. })({ Promise, requestAnimationFrame, IntersectionObserver });

QingJ © 2025

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