YouTube: Floating Chat Window on Fullscreen

在全屏上显示浮动聊天窗口

  1. // ==UserScript==
  2. // @name YouTube: Floating Chat Window on Fullscreen
  3. // @namespace UserScript
  4. // @version 0.5.5
  5. // @license MIT License
  6. // @author CY Fung
  7. // @match https://www.youtube.com/*
  8. // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
  9. // @require https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@5d83d154956057bdde19e24f95b332cb9a78fcda/library/default-trusted-type-policy.js
  10. // @require https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@8fac46500c5a916e6ed21149f6c25f8d1c56a6a3/library/ytZara.js
  11. // @run-at document-start
  12. // @grant none
  13. // @unwrap
  14. // @allFrames true
  15. // @inject-into page
  16. // @description To make a floating chat window on fullscreen
  17. // @description:ja フルスクリーンで浮動チャットウィンドウを表示する
  18. // @description:zh-TW 在全螢幕上顯示浮動聊天視窗
  19. // @description:zh-CN 在全屏上显示浮动聊天窗口
  20. // ==/UserScript==
  21.  
  22.  
  23. ((__CONTEXT__) => {
  24.  
  25. let activeStyle = false;
  26.  
  27. let _lastStyleText = null;
  28. let tvc = 0;
  29.  
  30. const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);
  31.  
  32. let c27 = 0;
  33. let mouseDownActiveElement = null;
  34.  
  35. /** @type {globalThis.PromiseConstructor} */
  36. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  37.  
  38.  
  39. const HTMLElement_ = HTMLElement;
  40.  
  41. /**
  42. * @param {Element} elm
  43. * @param {string} selector
  44. * @returns {Element | null}
  45. * */
  46. const qsOne = (elm, selector) => {
  47. return HTMLElement_.prototype.querySelector.call(elm, selector);
  48. }
  49.  
  50. /**
  51. * @param {Element} elm
  52. * @param {string} selector
  53. * @returns {NodeListOf<Element>}
  54. * */
  55. const qsAll = (elm, selector) => {
  56. return HTMLElement_.prototype.querySelectorAll.call(elm, selector);
  57. }
  58.  
  59.  
  60. const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : (this instanceof Window ? this : window);
  61.  
  62. const hkey_script = 'vdnvorrwsksy';
  63. if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
  64. win[hkey_script] = true;
  65.  
  66. document.addEventListener('click', function (evt) {
  67.  
  68. if (!document.fullscreenElement) return;
  69.  
  70. let byPass = false;
  71.  
  72. if (Date.now() - c27 < 40) {
  73. byPass = true;
  74. } else {
  75. return;
  76. }
  77.  
  78. if (evt.target && evt.target.id === 'chat' && evt.target.nodeName.toLowerCase() === 'ytd-live-chat-frame') byPass = false;
  79. else if (evt.target && evt.target.nodeName.toLowerCase() === 'iframe') byPass = false;
  80.  
  81. if (byPass) {
  82. evt.stopPropagation();
  83. evt.stopImmediatePropagation();
  84. c27 = Date.now();
  85. }
  86. c27 = 0;
  87.  
  88. }, { capture: true, passive: false });
  89.  
  90. document.addEventListener('mousedown', function (evt) {
  91.  
  92. if (!document.fullscreenElement) return;
  93. let byPass = false;
  94. const activeElement = document.activeElement || 0;
  95. mouseDownActiveElement = null;
  96. if (activeElement.nodeName === 'IFRAME') {
  97. if (activeElement.matches('[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) iframe')) {
  98. byPass = true;
  99. mouseDownActiveElement = activeElement;
  100. }
  101. }
  102.  
  103. if (evt.target && evt.target.id === 'chat' && evt.target.nodeName.toLowerCase() === 'ytd-live-chat-frame') byPass = false;
  104. else if (evt.target && evt.target.nodeName.toLowerCase() === 'iframe') byPass = false;
  105.  
  106.  
  107. if (byPass) {
  108.  
  109. evt.stopPropagation();
  110. evt.stopImmediatePropagation();
  111. c27 = Date.now();
  112. } else {
  113. mouseDownActiveElement = null;
  114. }
  115. c27 = 0;
  116.  
  117.  
  118.  
  119. }, { capture: true, passive: false });
  120. document.addEventListener('mouseup', function (evt) {
  121.  
  122. if (!document.fullscreenElement) return;
  123. let mde = mouseDownActiveElement;
  124. mouseDownActiveElement = null;
  125.  
  126. if (!mde) return;
  127.  
  128. let byPass = false;
  129. const activeElement = mde || 0;
  130. if (activeElement.nodeName === 'IFRAME') {
  131. if (activeElement.matches('[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) iframe')) {
  132. byPass = true;
  133. }
  134. }
  135.  
  136. // if(Date.now()-c27 < 40 ) byPass = true;
  137. c27 = 0;
  138.  
  139. if (evt.target && evt.target.id === 'chat' && evt.target.nodeName.toLowerCase() === 'ytd-live-chat-frame') byPass = false;
  140. else if (evt.target && evt.target.nodeName.toLowerCase() === 'iframe') byPass = false;
  141.  
  142. if (byPass) {
  143. evt.stopPropagation();
  144. evt.stopImmediatePropagation();
  145. c27 = Date.now();
  146. }
  147.  
  148. }, { capture: true, passive: false });
  149.  
  150. const svgDefs = () => `
  151. <svg version="1.1" xmlns="//www.w3.org/2000/svg" xmlns:xlink="//www.w3.org/1999/xlink" style="display:none;">
  152. <defs>
  153. <filter id="stroke-text-svg-filter-03">
  154.  
  155. <feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" result="white-text"/>
  156. <feMorphology in="white-text" result="DILATED" operator="dilate" radius="2"></feMorphology>
  157. <feFlood flood-color="transparent" flood-opacity="1" result="PINK" id="floodColor-03"></feFlood>
  158. <feComposite in="PINK" in2="DILATED" operator="in" result="OUTLINE"></feComposite>
  159. <feMerge>
  160. <feMergeNode in="OUTLINE" />
  161. <feMergeNode in="SourceGraphic" />
  162. </feMerge>
  163. </filter>
  164. <filter id="stroke-text-svg-filter-04">
  165. <feMorphology operator="dilate" radius="2"></feMorphology>
  166. <feComposite operator="xor" in="SourceGraphic"/>
  167. </filter>
  168. </defs>
  169.  
  170. </svg>
  171. `.trim();
  172.  
  173.  
  174. const createStyleTextForTopWin = () => `
  175. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) {
  176. position:fixed !important;
  177. top: var(--f3-top, 5px) !important;
  178. left: var(--f3-left, calc(60vw + 100px)) !important;
  179. height: var(--f3-h, 60vh) !important;
  180. width: var(--f3-w, 320px) !important;
  181. display:flex !important;
  182. flex-direction: column !important;
  183. padding: 4px;
  184. cursor: all-scroll;
  185. z-index:9999;
  186. box-sizing: border-box !important;
  187. margin:0 !important;
  188. opacity: var(--floating-window-opacity, 1.0) !important;
  189. background: transparent;
  190. background-color: rgba(0, 0, 0, 0.5);
  191. transition: background-color 300ms;
  192. }
  193. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]):hover {
  194. background-color: rgba(0, 0, 0, 0.85);
  195. }
  196. .no-floating[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) {
  197. top: -300vh !important;
  198. left: -300vh !important;
  199. }
  200. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) #show-hide-button[class]{
  201. flex-grow: 0;
  202. flex-shrink:0;
  203. position:static;
  204. cursor: all-scroll;
  205. }
  206. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) #show-hide-button[class] *[class]{
  207. cursor: inherit;
  208. }
  209. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) iframe[class]{
  210. flex-grow: 100;
  211. flex-shrink:0;
  212. height: 0;
  213. position:static;
  214. }
  215. html{
  216. --fc7-handle-color: #0cb8da;
  217. }
  218. html[dark]{
  219. --fc7-handle-color: #0c74e4;
  220. }
  221. :fullscreen .resize-handle {
  222. position: absolute !important;
  223. top: 0;
  224. left: 0;
  225. bottom: 0;
  226. background: transparent;
  227. right: 0;
  228. z-index: 999 !important;
  229. border-radius: inherit !important;
  230. box-sizing: border-box !important;
  231. pointer-events:none !important;
  232. visibility: collapse;
  233. border: 4px solid transparent;
  234. border-color: transparent;
  235. transition: border-color 300ms;
  236. }
  237. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]):hover .resize-handle {
  238. visibility: visible;
  239. border-color: var(--fc7-handle-color);
  240. }
  241. [moving] {
  242. cursor: all-scroll;
  243. --pointer-events:initial;
  244. }
  245. [moving] body {
  246. --pointer-events:none;
  247. }
  248. [moving] ytd-live-chat-frame#chat{
  249. --pointer-events:initial;
  250. }
  251. [moving] ytd-live-chat-frame#chat iframe {
  252. --pointer-events:none;
  253. }
  254. [moving="move"] ytd-live-chat-frame#chat {
  255. background-color: var(--yt-spec-general-background-a);
  256. }
  257. [moving="move"] ytd-live-chat-frame#chat iframe {
  258. visibility: collapse;
  259. }
  260. [moving] * {
  261. pointer-events:var(--pointer-events) !important;
  262. }
  263. [moving] *, [moving] [class] {
  264. user-select: none !important;
  265. }
  266. :fullscreen tyt-iframe-popup-btn {
  267. display: none !important;
  268. }
  269. [moving] tyt-iframe-popup-btn {
  270. display: none !important;
  271. }
  272. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) #show-hide-button.ytd-live-chat-frame>ytd-toggle-button-renderer.ytd-live-chat-frame {
  273. background: transparent;
  274. }
  275.  
  276. :fullscreen ytd-live-chat-frame#chat:not([collapsed]) {
  277. --chat-show-button-display: block;
  278. --chat-show-text-display: none;
  279. --chat-show-btn-text: 'ϞϞϞϞϞϞϞϞϞϞϞ';
  280. }
  281. :fullscreen ytd-live-chat-frame#chat:not([collapsed]) [is-show-button] [role="text"] {
  282. display: var(--chat-show-text-display);
  283. }
  284. :fullscreen ytd-live-chat-frame#chat:not([collapsed]) [is-show-button] button::before {
  285. content: var(--chat-show-btn-text);
  286. }
  287.  
  288. :fullscreen ytd-live-chat-frame#chat:not([collapsed]) [is-show-button][hidden] {
  289. display: var(--chat-show-button-display) !important;
  290. }
  291. `;
  292.  
  293. const createStyleTextForIframe = () => `
  294. .youtube-floating-chat-iframe yt-live-chat-docked-message#docked-messages.style-scope.yt-live-chat-item-list-renderer {
  295. margin-top:var(--fc7-top-banner-mt);
  296. transition: margin-top 180ms;
  297. }
  298. .youtube-floating-chat-iframe yt-live-chat-banner-manager#live-chat-banner.style-scope.yt-live-chat-item-list-renderer {
  299. margin-top:var(--fc7-top-banner-mt);
  300. transition: margin-top 180ms;
  301. }
  302. .youtube-floating-chat-iframe #action-panel.style-scope.yt-live-chat-renderer {
  303. position: fixed;
  304. top: 50%;
  305. transform: translateY(-50%);
  306. }
  307. .youtube-floating-chat-iframe yt-live-chat-header-renderer.style-scope.yt-live-chat-renderer {
  308. position: relative;
  309. z-index: 8;
  310. background: rgb(0,0,0);
  311. visibility: var(--fc7-panel-visibility);
  312. }
  313. .youtube-floating-chat-iframe #chat-messages.style-scope.yt-live-chat-renderer.iron-selected > #contents.style-scope.yt-live-chat-renderer {
  314. position: fixed;
  315. z-index: 4;
  316. top: 0;
  317. bottom: 0;
  318. left: 0;
  319. right: 0;
  320. }
  321. .youtube-floating-chat-iframe #right-arrow-container.yt-live-chat-ticker-renderer,
  322. .youtube-floating-chat-iframe #left-arrow-container.yt-live-chat-ticker-renderer
  323. {
  324. background: transparent;
  325. }
  326. .youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app {
  327. --yt-live-chat-background-color: transparent;
  328. --yt-live-chat-action-panel-background-color: rgba(0, 0, 0, 0.08);
  329. --yt-live-chat-header-background-color: rgba(0, 0, 0, 0.18);
  330. --yt-spec-static-overlay-background-medium: rgba(0, 0, 0, 0.08);
  331. --yt-live-chat-banner-gradient-scrim: transparent;
  332. }
  333. .youtube-floating-chat-iframe{
  334. --fc7-top-banner-mt: 0px;
  335. --fc7-banner-opacity: 0.86;
  336. --fc7-system-message-opacity: 0.66;
  337. --fc7-system-message-opacity2: 0.66;
  338. --fc7-panel-display: none;
  339. --fc7-panel-visibility: collapse;
  340. --fc7-panel-position: absolute;
  341. }
  342.  
  343. .youtube-floating-chat-iframe:focus-within,
  344. html:focus-within,
  345. body:focus-within,
  346. yt-live-chat-app:focus-within,
  347. yt-live-chat-renderer.yt-live-chat-app:focus-within
  348. {
  349. --fc7-top-banner-mt: 56px;
  350. --fc7-banner-opacity: 1.0;
  351. --fc7-system-message-opacity: 1.0;
  352. --fc7-system-message-opacity2: 1.00;
  353. --fc7-panel-display: invalid;
  354. --fc7-panel-visibility: invalid;
  355. --fc7-panel-position: absolute;
  356. }
  357. .youtube-floating-chat-iframe yt-live-chat-app:hover {
  358. --fc7-top-banner-mt: 56px;
  359. --fc7-banner-opacity: 1.0;
  360. --fc7-system-message-opacity: 1.0;
  361. --fc7-system-message-opacity2: 1.00;
  362. --fc7-panel-display: invalid;
  363. --fc7-panel-visibility: invalid;
  364. --fc7-panel-position: absolute;
  365. }
  366. .youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app #visible-banners > yt-live-chat-banner-renderer {
  367. opacity: var(--fc7-banner-opacity) !important;
  368. }
  369. .youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app yt-live-chat-viewer-engagement-message-renderer {
  370. opacity: var(--fc7-system-message-opacity) !important;
  371. }
  372. .youtube-floating-chat-iframe yt-live-chat-app yt-live-chat-renderer.yt-live-chat-app yt-live-chat-message-input-renderer {
  373. visibility: var(--fc7-panel-visibility);
  374. position: var(--fc7-panel-position);
  375. transform: translateY(-100%);
  376. left: 0;
  377. right: 0;
  378. opacity: 1;
  379. background: rgba(0,0,0,0.86);
  380. }
  381. /* hide message with input panel hidden */
  382. .youtube-floating-chat-iframe yt-live-chat-app > tp-yt-iron-dropdown.yt-live-chat-app yt-tooltip-renderer[slot="dropdown-content"][position-type="OPEN_POPUP_POSITION_TOP"].yt-live-chat-app {
  383. visibility: var(--fc7-panel-visibility);
  384. }
  385. [dark].youtube-floating-chat-iframe yt-live-chat-app ::-webkit-scrollbar-track,
  386. [dark].youtube-floating-chat-iframe yt-live-chat-kevlar-container ::-webkit-scrollbar-track {
  387. background-color: var(--ytd-searchbox-legacy-button-color);
  388. }
  389. .youtube-floating-chat-iframe yt-live-chat-app ::-webkit-scrollbar-track,
  390. .youtube-floating-chat-iframe yt-live-chat-kevlar-container ::-webkit-scrollbar-track {
  391. background-color: #fcfcfc;
  392. }
  393. [dark].youtube-floating-chat-iframe yt-live-chat-app ::-webkit-scrollbar-thumb,
  394. [dark].youtube-floating-chat-iframe yt-live-chat-kevlar-container ::-webkit-scrollbar-thumb{
  395. background-color: var(--ytd-searchbox-legacy-button-color);
  396. border: 2px solid var(--ytd-searchbox-legacy-button-color);
  397. }
  398. .youtube-floating-chat-iframe yt-live-chat-renderer[has-action-panel-renderer] #action-panel.yt-live-chat-renderer {
  399. --yt-live-chat-action-panel-gradient-scrim: transparent;
  400. }
  401. .youtube-floating-chat-iframe yt-live-chat-renderer[has-action-panel-renderer] #action-panel.yt-live-chat-renderer yt-live-chat-action-panel-renderer {
  402. opacity: var(--fc7-system-message-opacity2) !important;
  403. }
  404. `;
  405.  
  406. const { isIframe, isTopFrame } = (() => {
  407.  
  408. let isIframe = false, isTopFrame = false;
  409. try {
  410. isIframe = window.document !== top.document
  411. } catch (e) { }
  412.  
  413. try {
  414. isTopFrame = window.document === top.document
  415. } catch (e) { }
  416.  
  417. return { isIframe, isTopFrame };
  418.  
  419. })();
  420.  
  421. if (isIframe ^ isTopFrame) { } else return;
  422.  
  423. const addCSS = (createStyleText) => {
  424. let text = createStyleText();
  425. let style = document.createElement('style');
  426. style.id = 'rvZ0t';
  427. style.textContent = text;
  428. document.head.appendChild(style);
  429. }
  430.  
  431. const cleanContext = async (win) => {
  432. const waitFn = requestAnimationFrame; // shall have been binded to window
  433. try {
  434. let mx = 16; // MAX TRIAL
  435. const frameId = 'vanillajs-iframe-v1'
  436. let frame = document.getElementById(frameId);
  437. let removeIframeFn = null;
  438. if (!frame) {
  439. frame = document.createElement('iframe');
  440. frame.id = 'vanillajs-iframe-v1';
  441. frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
  442. let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
  443. n.appendChild(frame);
  444. while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
  445. const root = document.documentElement;
  446. root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
  447. removeIframeFn = (setTimeout) => {
  448. const removeIframeOnDocumentReady = (e) => {
  449. e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
  450. win = null;
  451. setTimeout(() => {
  452. n.remove();
  453. n = null;
  454. }, 200);
  455. }
  456. if (document.readyState !== 'loading') {
  457. removeIframeOnDocumentReady();
  458. } else {
  459. win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
  460. }
  461. }
  462. }
  463. while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
  464. const fc = frame.contentWindow;
  465. if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
  466. const { requestAnimationFrame, setTimeout } = fc;
  467. const res = { requestAnimationFrame, setTimeout };
  468. for (let k in res) res[k] = res[k].bind(win); // necessary
  469. if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
  470. return res;
  471. } catch (e) {
  472. console.warn(e);
  473. return null;
  474. }
  475. };
  476.  
  477. isTopFrame && cleanContext(win).then(__CONTEXT__ => {
  478. if (!__CONTEXT__) return null;
  479.  
  480. const { requestAnimationFrame } = __CONTEXT__;
  481.  
  482. let chatWindowWR = null;
  483. let showHideButtonWR = null;
  484. let showButtonWR = null;
  485.  
  486. /* globals WeakRef:false */
  487.  
  488. /** @type {(o: Object | null) => WeakRef | null} */
  489. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  490.  
  491. /** @type {(wr: Object | null) => Object | null} */
  492. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  493.  
  494. let rafPromise = null;
  495.  
  496. const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => {
  497. requestAnimationFrame(hRes => {
  498. rafPromise = null;
  499. resolve(hRes);
  500. });
  501. }));
  502.  
  503. let startX;
  504. let startY;
  505. let startWidth;
  506. let startHeight;
  507.  
  508. let edge = 0;
  509.  
  510. let initialLeft;
  511. let initialTop;
  512.  
  513. let stopResize;
  514. let stopMove;
  515.  
  516. function filteroutHidden(el) {
  517. if (!el) return el;
  518. if (el.closest('[hidden]')) return null;
  519. return el;
  520. }
  521.  
  522. const getXY = (e) => {
  523. let rect = e.target.getBoundingClientRect();
  524. let x = e.clientX - rect.left; //x position within the element.
  525. let y = e.clientY - rect.top; //y position within the element.
  526. return { x, y };
  527. }
  528.  
  529. let beforeEvent = null;
  530.  
  531. function resizeWindow(e) {
  532.  
  533.  
  534. const chatWindow = kRef(chatWindowWR);
  535. if (chatWindow) {
  536.  
  537. const mEdge = edge;
  538.  
  539. if (mEdge == 4 || mEdge == 1) {
  540. } else if (mEdge == 8 || mEdge == 16) {
  541. } else {
  542. return;
  543. }
  544.  
  545. Promise.resolve(chatWindow).then(chatWindow => {
  546. let rect;
  547.  
  548. if (mEdge == 4 || mEdge == 1 || mEdge == 16) {
  549.  
  550. let newWidth = startWidth + (startX - e.pageX);
  551. let newLeft = initialLeft + startWidth - newWidth;
  552. chatWindow.style.setProperty('--f3-w', newWidth + "px");
  553. chatWindow.style.setProperty('--f3-left', newLeft + "px");
  554.  
  555. let newHeight = startHeight + (startY - e.pageY);
  556. let newTop = initialTop + startHeight - newHeight;
  557. chatWindow.style.setProperty('--f3-h', newHeight + "px");
  558. chatWindow.style.setProperty('--f3-top', newTop + "px");
  559.  
  560. rect = {
  561. x: newLeft,
  562. y: newTop,
  563. w: newWidth,
  564. h: newHeight,
  565. };
  566.  
  567. } else if (mEdge == 8) {
  568.  
  569. let newWidth = startWidth + e.pageX - startX;
  570. let newHeight = startHeight + e.pageY - startY;
  571. chatWindow.style.setProperty('--f3-w', newWidth + "px");
  572. chatWindow.style.setProperty('--f3-h', newHeight + "px");
  573.  
  574. rect = {
  575. x: initialLeft,
  576. y: initialTop,
  577. w: newWidth,
  578. h: newHeight,
  579. };
  580.  
  581. }
  582.  
  583. updateOpacity(chatWindow, rect, screen);
  584.  
  585. })
  586.  
  587. e.stopImmediatePropagation();
  588. e.stopPropagation();
  589. e.preventDefault();
  590.  
  591.  
  592. }
  593.  
  594. }
  595.  
  596. let isMoved = false;
  597.  
  598. function moveWindow(e) {
  599.  
  600. const chatWindow = kRef(chatWindowWR);
  601. if (!chatWindow) return;
  602.  
  603. Promise.resolve(chatWindow).then(chatWindow => {
  604.  
  605. let newX = initialLeft + e.pageX - startX;
  606. let newY = initialTop + e.pageY - startY;
  607.  
  608. if (Math.abs(e.pageX - startX) > 10 || Math.abs(e.pageY - startY) > 10) isMoved = true;
  609.  
  610. chatWindow.style.setProperty('--f3-left', newX + "px");
  611. chatWindow.style.setProperty('--f3-top', newY + "px");
  612.  
  613. updateOpacity(chatWindow, {
  614. x: newX,
  615. y: newY,
  616. w: startWidth,
  617. h: startHeight,
  618. }, screen);
  619.  
  620. });
  621.  
  622. e.stopImmediatePropagation();
  623. e.stopPropagation();
  624. e.preventDefault();
  625.  
  626.  
  627. }
  628.  
  629.  
  630. function initializeResize(e) {
  631.  
  632. if (!document.fullscreenElement) return;
  633.  
  634. if (!document.querySelector('[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed])')) return;
  635.  
  636. if (e.target.id !== 'chat') return;
  637.  
  638. const { x, y } = getXY(e);
  639. edge = 0;
  640. if (x < 16 && y < 16) { edge = 16; }
  641. else if (x < 16) edge = 4;
  642. else if (y < 16) edge = 1;
  643. else edge = 8;
  644.  
  645. if (edge <= 0) return;
  646.  
  647. startX = e.pageX;
  648. startY = e.pageY;
  649.  
  650. const chatWindow = kRef(chatWindowWR);
  651. if (chatWindow) {
  652. Promise.resolve(chatWindow).then(chatWindow => {
  653.  
  654. const rect = chatWindow.getBoundingClientRect();
  655. initialLeft = rect.x;
  656. initialTop = rect.y;
  657.  
  658. startWidth = rect.width;
  659. startHeight = rect.height;
  660.  
  661. chatWindow.style.setProperty('--f3-left', initialLeft + "px");
  662. chatWindow.style.setProperty('--f3-top', initialTop + "px");
  663. chatWindow.style.setProperty('--f3-w', startWidth + "px");
  664. chatWindow.style.setProperty('--f3-h', startHeight + "px");
  665.  
  666. });
  667. }
  668.  
  669. document.documentElement.setAttribute('moving', 'resize');
  670.  
  671. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  672. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  673. document.documentElement.removeEventListener("mouseup", stopResize, false);
  674. document.documentElement.removeEventListener("mouseup", stopMove, false);
  675.  
  676. isMoved = false;
  677. document.documentElement.addEventListener("mousemove", resizeWindow);
  678. document.documentElement.addEventListener("mouseup", stopResize);
  679.  
  680. }
  681.  
  682.  
  683. let updateOpacityRid = 0;
  684.  
  685. function updateOpacity(chatWindow, rect, screen) {
  686.  
  687. const tid = ++updateOpacityRid;
  688.  
  689. getRafPromise().then(() => {
  690.  
  691. if (tid !== updateOpacityRid) return;
  692.  
  693. const { x, y, w, h } = rect;
  694. const [left, top, right, bottom] = [x, y, x + w, y + h];
  695.  
  696. const opacityW = (Math.min(right, screen.width) - Math.max(0, left)) / w;
  697. const opacityH = (Math.min(bottom, screen.height) - Math.max(0, top)) / h;
  698.  
  699. const opacity = Math.min(opacityW, opacityH);
  700.  
  701. chatWindow.style.setProperty('--floating-window-opacity', Math.round(opacity * 100 * 5, 0) / 5 / 100);
  702.  
  703. });
  704.  
  705. }
  706.  
  707. function initializeMove(e) {
  708.  
  709. if (!document.fullscreenElement) return;
  710. if (!document.querySelector('[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed])')) return;
  711.  
  712. const chatWindow = kRef(chatWindowWR);
  713.  
  714. startX = e.pageX;
  715. startY = e.pageY;
  716.  
  717. if (chatWindow) {
  718.  
  719. Promise.resolve(chatWindow).then(chatWindow => {
  720.  
  721. let rect = chatWindow.getBoundingClientRect();
  722. initialLeft = rect.x;
  723. initialTop = rect.y;
  724.  
  725. startWidth = rect.width;
  726. startHeight = rect.height;
  727.  
  728. chatWindow.style.setProperty('--f3-left', initialLeft + "px");
  729. chatWindow.style.setProperty('--f3-top', initialTop + "px");
  730. chatWindow.style.setProperty('--f3-w', startWidth + "px");
  731. chatWindow.style.setProperty('--f3-h', startHeight + "px");
  732.  
  733. });
  734.  
  735. }
  736.  
  737. document.documentElement.setAttribute('moving', 'move');
  738.  
  739. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  740. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  741. document.documentElement.removeEventListener("mouseup", stopResize, false);
  742. document.documentElement.removeEventListener("mouseup", stopMove, false);
  743. isMoved = false;
  744.  
  745. document.documentElement.addEventListener("mousemove", moveWindow, false);
  746. document.documentElement.addEventListener("mouseup", stopMove, false);
  747.  
  748. e.stopImmediatePropagation();
  749. e.stopPropagation();
  750. e.preventDefault();
  751.  
  752. beforeEvent = e;
  753.  
  754. }
  755.  
  756.  
  757. function checkClick(beforeEvent, currentEvent) {
  758.  
  759. const d = currentEvent.timeStamp - beforeEvent.timeStamp;
  760. if (d < 300 && d > 30 && !isMoved) {
  761. document.documentElement.classList.add('no-floating');
  762. }
  763.  
  764. }
  765.  
  766. stopResize = (e) => {
  767.  
  768. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  769. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  770. document.documentElement.removeEventListener("mouseup", stopResize, false);
  771. document.documentElement.removeEventListener("mouseup", stopMove, false);
  772.  
  773. document.documentElement.removeAttribute('moving');
  774.  
  775. e.stopImmediatePropagation();
  776. e.stopPropagation();
  777.  
  778. }
  779.  
  780. stopMove = (e) => {
  781.  
  782. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  783. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  784. document.documentElement.removeEventListener("mouseup", stopResize, false);
  785. document.documentElement.removeEventListener("mouseup", stopMove, false);
  786.  
  787. document.documentElement.removeAttribute('moving');
  788. beforeEvent && checkClick(beforeEvent, e);
  789. beforeEvent = null;
  790.  
  791. e.stopImmediatePropagation();
  792. e.stopPropagation();
  793.  
  794. }
  795.  
  796. function reset() {
  797.  
  798. document.documentElement.removeAttribute('moving');
  799. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  800. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  801. document.documentElement.removeEventListener("mouseup", stopResize, false);
  802. document.documentElement.removeEventListener("mouseup", stopMove, false);
  803.  
  804. startX = 0;
  805. startY = 0;
  806. startWidth = 0;
  807. startHeight = 0;
  808.  
  809. edge = 0;
  810.  
  811. initialLeft = 0;
  812. initialTop = 0;
  813.  
  814. beforeEvent = null;
  815.  
  816. }
  817.  
  818. function iframeFullscreenChanged() {
  819. const iframeDoc = this;
  820.  
  821.  
  822. _lastStyleText = null;
  823.  
  824. if (!document.fullscreenElement) {
  825. activeStyle = false;
  826. iframeDoc.documentElement.classList.remove('youtube-floating-chat-iframe');
  827. } else {
  828. activeStyle = true;
  829. iframeDoc.documentElement.classList.add('youtube-floating-chat-iframe');
  830.  
  831. }
  832.  
  833. }
  834.  
  835. let iframeFullscreenChangedBinded = null;
  836.  
  837. function onMessage(evt) {
  838. if (evt.data !== hkey_script) return;
  839.  
  840. const iframeWin = evt.source;
  841. if (!iframeWin) return;
  842. const iframeDoc = iframeWin.document;
  843.  
  844. const intervalCheckFn = () => {
  845.  
  846. if (!activeStyle) return;
  847.  
  848. let xpathExpression = "//style[text()[contains(., 'userscript-control[floating-chat-iframe]')]]";
  849.  
  850. // Evaluating the XPath expression and getting string value directly
  851. let result = iframeDoc.evaluate(xpathExpression, iframeDoc, null, XPathResult.STRING_TYPE, null);
  852.  
  853. let newText = result && result.stringValue ? result.stringValue : null;
  854.  
  855. if (newText !== _lastStyleText) {
  856. _lastStyleText = newText;
  857. // console.log(123)
  858.  
  859. let tid = ++tvc;
  860.  
  861. getRafPromise().then(() => {
  862.  
  863. if (tid !== tvc) return;
  864.  
  865. let style = iframeWin.getComputedStyle(iframeDoc.documentElement);
  866.  
  867. let fc = style.getPropertyValue('--floodcolor');
  868. if (fc) {
  869.  
  870. let floodColor03 = iframeDoc.querySelector('#floodColor-03');
  871. floodColor03 && floodColor03.setAttribute('flood-color', fc);
  872.  
  873. let floodColor04 = iframeDoc.querySelector('#floodColor-04');
  874. floodColor04 && floodColor04.setAttribute('flood-color', fc);
  875.  
  876. iframeDoc.documentElement.setAttribute('hpkns', '')
  877. } else {
  878. iframeDoc.documentElement.removeAttribute('hpkns')
  879.  
  880. }
  881.  
  882. });
  883.  
  884. }
  885.  
  886. };
  887.  
  888. function onReady() {
  889.  
  890. iframeDoc.head.appendChild(document.createElement('style')).textContent = createStyleTextForIframe();
  891.  
  892. const tm = document.createElement('template');
  893. tm.innerHTML = svgDefs();
  894. iframeDoc.body.appendChild(tm.content)
  895.  
  896. if (iframeFullscreenChangedBinded) document.removeEventListener('fullscreenchange', iframeFullscreenChangedBinded, false);
  897. iframeFullscreenChangedBinded = iframeFullscreenChanged.bind(iframeDoc);
  898. document.addEventListener('fullscreenchange', iframeFullscreenChangedBinded, false);
  899.  
  900. iframeFullscreenChangedBinded();
  901.  
  902. setInterval(intervalCheckFn, 100);
  903.  
  904. }
  905.  
  906. Promise.resolve().then(() => {
  907.  
  908. if (iframeDoc.readyState !== 'loading') {
  909. onReady();
  910. } else {
  911. iframeWin.addEventListener("DOMContentLoaded", onReady, false);
  912. }
  913.  
  914. });
  915.  
  916.  
  917.  
  918. }
  919.  
  920. let moInt = 0;
  921. const mutationObserverFn = () => {
  922. if (moInt > 1e9) moInt = 9;
  923. const t = ++moInt;
  924. getRafPromise().then(() => {
  925. if (t !== moInt) return;
  926.  
  927. const chatWindow = chatWindowWR ? kRef(chatWindowWR) : null;
  928.  
  929. if (!(chatWindow instanceof Element)) return;
  930. if (chatWindow.hasAttribute('collapsed')) return;
  931. const chatWindowCnt = insp(chatWindow);
  932.  
  933. if (!chatWindowCnt) return;
  934.  
  935. const btn = qsOne(chatWindow, '#show-hide-button[hidden]')
  936. if (!btn) return;
  937.  
  938. if (btn && filteroutHidden(chatWindow)) {
  939. const liveChatRenderer = ((chatWindowCnt || 0).data || 0).liveChatRenderer || 0;
  940. if (liveChatRenderer && liveChatRenderer.showButton && !liveChatRenderer.showHideButton) {
  941. btn.setAttribute('is-show-button', '')
  942. } else {
  943. btn.removeAttribute('is-show-button')
  944. }
  945. }
  946.  
  947. });
  948.  
  949. };
  950. const mutationObserver = new MutationObserver(mutationObserverFn);
  951.  
  952. function setChat(chat) {
  953.  
  954. if (!(chat instanceof Element)) return;
  955. let resizeHandle = qsOne(chat, '.resize-handle')
  956. if (resizeHandle) return;
  957.  
  958. const chatDollar = insp(chat).$ || chat.$ || 0;
  959.  
  960. let cw = (() => {
  961. try {
  962. const { head, body } = chatDollar.chatframe.contentWindow.document;
  963. return { head, body }
  964.  
  965. } catch (e) { return null; }
  966. })();
  967.  
  968. if (!cw) return;
  969.  
  970. window.removeEventListener('message', onMessage, false);
  971. window.addEventListener('message', onMessage, false);
  972.  
  973.  
  974.  
  975. let script = document.getElementById('rvZ0t') || (document.evaluate("//div[contains(text(), 'userscript-control[enable-customized-floating-window]')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null) || 0).singleNodeValue;
  976. if (!script) addCSS(createStyleTextForTopWin);
  977. /*
  978. const tm = document.createElement('template');
  979. tm.innerHTML=svgDefs();
  980. document.body.appendChild(tm.content)
  981. */
  982.  
  983. if (!document.documentElement.hasAttribute('floating-chat-window')) document.documentElement.setAttribute('floating-chat-window', '');
  984.  
  985.  
  986. chat.setAttribute('allowtransparency', 'true');
  987.  
  988.  
  989.  
  990. resizeHandle = document.createElement("div");
  991. resizeHandle.className = "resize-handle";
  992. chat.appendChild(resizeHandle);
  993. resizeHandle = null;
  994.  
  995. let chatWindow;
  996. let showHideButton;
  997. let showButton;
  998.  
  999. chatWindow = kRef(chatWindowWR);
  1000. showHideButton = kRef(showHideButtonWR);
  1001. showButton = kRef(showButtonWR);
  1002.  
  1003.  
  1004.  
  1005. if (chatWindow) chatWindow.removeEventListener("mousedown", initializeResize, false);
  1006. if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);
  1007. else if (showButton) showButton.removeEventListener("mousedown", initializeMove, false);
  1008.  
  1009. if (chat) {
  1010. mutationObserver.observe(chat, { attributes: true, attributeFilter: ['collapsed', 'hidden'] });
  1011. mutationObserverFn();
  1012. }
  1013.  
  1014.  
  1015. chatWindow = chat;
  1016. showHideButton = (qsOne(chat, '#show-hide-button'));
  1017. showButton = (qsOne(chat, '#show-button'));
  1018. chatWindowWR = mWeakRef(chat)
  1019. showHideButtonWR = mWeakRef(showHideButton);
  1020. showButtonWR = mWeakRef(showButton);
  1021.  
  1022.  
  1023.  
  1024. if (chatWindow) chatWindow.addEventListener("mousedown", initializeResize, false);
  1025. if (showHideButton) showHideButton.addEventListener("mousedown", initializeMove, false);
  1026. else if (showButton) showButton.addEventListener("mousedown", initializeMove, false);
  1027.  
  1028. reset();
  1029.  
  1030. }
  1031.  
  1032.  
  1033. const fullscreenchangePageFn = () => {
  1034. if (!document.fullscreenElement) {
  1035. document.documentElement.classList.remove('no-floating')
  1036. }
  1037. };
  1038.  
  1039.  
  1040. function noChat(chat) {
  1041.  
  1042. if (!(chat instanceof Element)) return;
  1043.  
  1044. let chatWindow;
  1045. let showHideButton;
  1046. let showButton;
  1047.  
  1048. chatWindow = kRef(chatWindowWR);
  1049. showHideButton = kRef(showHideButtonWR);
  1050. showButton = kRef(showButtonWR);
  1051.  
  1052.  
  1053.  
  1054. if (chatWindow) chatWindow.removeEventListener("mousedown", initializeResize, false);
  1055. if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);
  1056. else if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);
  1057.  
  1058.  
  1059. let resizeHandle = qsOne(chat, '.resize-handle')
  1060. if (resizeHandle) {
  1061. resizeHandle.remove();
  1062. }
  1063.  
  1064. chat.removeEventListener("mousedown", initializeResize, false);
  1065.  
  1066.  
  1067. showHideButton = (qsOne(chat, '#show-hide-button'));
  1068. showButton = (qsOne(chat, '#show-button'));
  1069.  
  1070. if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);
  1071. else if (showButton) showButton.removeEventListener("mousedown", initializeMove, false);
  1072.  
  1073.  
  1074. reset();
  1075. }
  1076.  
  1077.  
  1078. document.removeEventListener('fullscreenchange', fullscreenchangePageFn, false);
  1079. document.addEventListener('fullscreenchange', fullscreenchangePageFn, false);
  1080. fullscreenchangePageFn();
  1081.  
  1082. ytZara.ytProtoAsync("ytd-live-chat-frame").then((proto) => {
  1083.  
  1084. proto.attached = ((attached) => (function () { Promise.resolve(this.hostElement || this).then(setChat).catch(console.warn); return attached.apply(this, arguments) }))(proto.attached);
  1085.  
  1086. proto.detached = ((detached) => (function () { Promise.resolve(this.hostElement || this).then(noChat).catch(console.warn); return detached.apply(this, arguments) }))(proto.detached);
  1087.  
  1088. let chat = document.querySelector('ytd-live-chat-frame');
  1089. if (chat) Promise.resolve(chat).then(setChat).catch(console.warn);
  1090.  
  1091. });
  1092.  
  1093.  
  1094. });
  1095.  
  1096. (!isTopFrame && isIframe && top === parent) && top.postMessage(hkey_script, `${location.protocol}//${location.hostname}`);
  1097.  
  1098.  
  1099.  
  1100. })({ requestAnimationFrame });