YouTube: Floating Chat Window on Fullscreen

To make floating chat window on fullscreen

目前为 2023-07-20 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube: Floating Chat Window on Fullscreen
  3. // @namespace UserScript
  4. // @match https://www.youtube.com/*
  5. // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
  6. // @version 0.1.7
  7. // @license MIT License
  8. // @author CY Fung
  9. // @description To make floating chat window on fullscreen
  10. // @require https://greasyfork.org/scripts/465819-api-for-customelements-in-youtube/code/API%20for%20CustomElements%20in%20YouTube.js?version=1215161
  11. // @run-at document-start
  12. // @grant none
  13. // @unwrap
  14. // @allFrames true
  15. // @inject-into page
  16. // ==/UserScript==
  17.  
  18.  
  19. ((__CONTEXT__) => {
  20.  
  21. const createStyleText = () => `
  22.  
  23.  
  24. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) {
  25. position:fixed !important;
  26. top: var(--f3-top, 5px) !important;
  27. left: var(--f3-left, calc(60vw + 100px)) !important;
  28. height: var(--f3-h, 60vh) !important;
  29. width: var(--f3-w, 320px) !important;
  30. display:flex !important;
  31. flex-direction: column !important;
  32. padding: 4px;
  33. cursor: all-scroll;
  34. z-index:9999;
  35. box-sizing: border-box !important;
  36. margin:0 !important;
  37. opacity: var(--floating-window-opacity, 1.0) !important;
  38. }
  39.  
  40. .no-floating[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) {
  41.  
  42. top: -300vh !important;
  43. left: -300vh !important;
  44. }
  45.  
  46.  
  47. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) #show-hide-button[class]{
  48. flex-grow: 0;
  49. flex-shrink:0;
  50. position:static;
  51. cursor: all-scroll;
  52. }
  53. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) #show-hide-button[class] *[class]{
  54. cursor: inherit;
  55. }
  56. [floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) iframe[class]{
  57. flex-grow: 100;
  58. flex-shrink:0;
  59. height: 0;
  60. position:static;
  61. }
  62.  
  63.  
  64. html{
  65. --f7-handle-color: #0cb8da;
  66. }
  67. html[dark]{
  68. --f7-handle-color: #0c74e4;
  69. }
  70. :fullscreen .resize-handle{
  71.  
  72. position: absolute;
  73. top: 0;
  74. left: 0;
  75. bottom: 0;
  76. background: transparent;
  77. right: 0;
  78. border: 4px solid var(--f7-handle-color);
  79. z-index: 999;
  80. border-radius: inherit;
  81. box-sizing: border-box;
  82. pointer-events:none;
  83. }
  84.  
  85. [moving] {
  86. cursor: all-scroll;
  87. --pointer-events:initial;
  88. }
  89.  
  90. [moving] body {
  91. --pointer-events:none;
  92. }
  93.  
  94. [moving] ytd-live-chat-frame#chat{
  95.  
  96. --pointer-events:initial;
  97. }
  98.  
  99.  
  100. [moving] ytd-live-chat-frame#chat iframe {
  101.  
  102. --pointer-events:none;
  103. }
  104.  
  105.  
  106. [moving="move"] ytd-live-chat-frame#chat {
  107. background-color: var(--yt-spec-general-background-a);
  108.  
  109. }
  110.  
  111.  
  112. [moving="move"] ytd-live-chat-frame#chat iframe {
  113.  
  114. visibility: collapse;
  115. }
  116.  
  117. [moving] * {
  118. pointer-events:var(--pointer-events) !important;
  119.  
  120. }
  121.  
  122. :fullscreen tyt-iframe-popup-btn{
  123. display: none !important;
  124. }
  125.  
  126. [moving] tyt-iframe-popup-btn{
  127. display: none !important;
  128. }
  129.  
  130.  
  131.  
  132. `;
  133.  
  134. const addCSS = () => {
  135. let text = createStyleText();
  136. let style = document.createElement('style');
  137. style.id = 'rvZ0t';
  138. style.textContent = text;
  139. document.head.appendChild(style);
  140. }
  141.  
  142. const { Promise, requestAnimationFrame } = __CONTEXT__;
  143.  
  144. let chatWindowWR = null;
  145. let showHideButtonWR = null;
  146.  
  147.  
  148. /* globals WeakRef:false */
  149.  
  150. /** @type {(o: Object | null) => WeakRef | null} */
  151. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  152.  
  153. /** @type {(wr: Object | null) => Object | null} */
  154. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  155.  
  156.  
  157.  
  158. let startX;
  159. let startY;
  160. let startWidth;
  161. let startHeight;
  162.  
  163.  
  164. let edge = 0;
  165.  
  166.  
  167. let initialLeft;
  168. let initialTop;
  169.  
  170. let stopResize;
  171. let stopMove;
  172.  
  173.  
  174. const getXY = (e) => {
  175. let rect = e.target.getBoundingClientRect();
  176. let x = e.clientX - rect.left; //x position within the element.
  177. let y = e.clientY - rect.top; //y position within the element.
  178. return { x, y }
  179. }
  180.  
  181. let beforeEvent = null;
  182.  
  183. function resizeWindow(e) {
  184.  
  185.  
  186. const chatWindow = kRef(chatWindowWR);
  187. if (chatWindow) {
  188.  
  189. const mEdge = edge;
  190.  
  191. if (mEdge == 4 || mEdge == 1) {
  192.  
  193. } else if (mEdge == 8) {
  194. } else {
  195. return;
  196. }
  197.  
  198.  
  199. Promise.resolve(chatWindow).then(chatWindow => {
  200. let rect;
  201.  
  202. if (mEdge == 4 || mEdge == 1) {
  203.  
  204. let newWidth = startWidth + (startX - e.pageX);
  205.  
  206. let newLeft = initialLeft + startWidth - newWidth;
  207. chatWindow.style.setProperty('--f3-w', newWidth + "px");
  208. chatWindow.style.setProperty('--f3-left', newLeft + "px");
  209.  
  210.  
  211.  
  212. let newHeight = startHeight + (startY - e.pageY);
  213.  
  214. let newTop = initialTop + startHeight - newHeight;
  215. chatWindow.style.setProperty('--f3-h', newHeight + "px");
  216. chatWindow.style.setProperty('--f3-top', newTop + "px");
  217.  
  218. rect = {
  219. x: newLeft,
  220. y: newTop,
  221. w: newWidth,
  222.  
  223. h: newHeight,
  224.  
  225.  
  226. };
  227.  
  228.  
  229.  
  230. } else if (mEdge == 8) {
  231.  
  232. let newWidth = startWidth + e.pageX - startX;
  233. let newHeight = startHeight + e.pageY - startY;
  234.  
  235. chatWindow.style.setProperty('--f3-w', newWidth + "px");
  236. chatWindow.style.setProperty('--f3-h', newHeight + "px");
  237.  
  238.  
  239. rect = {
  240. x: initialLeft,
  241. y: initialTop,
  242. w: newWidth,
  243.  
  244. h: newHeight,
  245.  
  246.  
  247. };
  248.  
  249. }
  250.  
  251.  
  252.  
  253. updateOpacity(chatWindow, rect, screen);
  254.  
  255. })
  256.  
  257.  
  258. e.stopPropagation();
  259. e.preventDefault();
  260.  
  261.  
  262. }
  263.  
  264. }
  265.  
  266. function moveWindow(e) {
  267.  
  268.  
  269. const chatWindow = kRef(chatWindowWR);
  270. if (chatWindow) {
  271.  
  272. Promise.resolve(chatWindow).then(chatWindow => {
  273.  
  274.  
  275. let newX = initialLeft + e.pageX - startX;
  276. let newY = initialTop + e.pageY - startY;
  277.  
  278. chatWindow.style.setProperty('--f3-left', newX + "px");
  279. chatWindow.style.setProperty('--f3-top', newY + "px");
  280.  
  281.  
  282.  
  283. updateOpacity(chatWindow, {
  284. x: newX,
  285. y: newY,
  286. w: startWidth,
  287.  
  288. h: startHeight,
  289.  
  290.  
  291. }, screen);
  292.  
  293. });
  294.  
  295.  
  296.  
  297. e.stopPropagation();
  298. e.preventDefault();
  299.  
  300. }
  301. }
  302.  
  303.  
  304.  
  305.  
  306. function initializeResize(e) {
  307.  
  308. if (!document.fullscreenElement) return;
  309.  
  310. if (e.target.id !== 'chat') return;
  311.  
  312.  
  313. const { x, y } = getXY(e);
  314. edge = 0;
  315. if (x < 16 && y < 16) { edge = -1 }
  316. else if (x < 16) edge = 4;
  317. else if (y < 16) edge = 1;
  318. else edge = 8;
  319.  
  320. if (edge <= 0) return;
  321.  
  322. startX = e.pageX;
  323. startY = e.pageY;
  324.  
  325. const chatWindow = kRef(chatWindowWR);
  326. if (chatWindow) {
  327.  
  328. Promise.resolve(chatWindow).then(chatWindow => {
  329.  
  330. let rect = chatWindow.getBoundingClientRect();
  331. initialLeft = rect.x;
  332. initialTop = rect.y;
  333.  
  334.  
  335.  
  336. startWidth = rect.width;
  337. startHeight = rect.height;
  338.  
  339.  
  340. chatWindow.style.setProperty('--f3-left', initialLeft + "px");
  341. chatWindow.style.setProperty('--f3-top', initialTop + "px");
  342. chatWindow.style.setProperty('--f3-w', startWidth + "px");
  343. chatWindow.style.setProperty('--f3-h', startHeight + "px");
  344.  
  345. });
  346.  
  347. }
  348.  
  349.  
  350.  
  351.  
  352. document.documentElement.setAttribute('moving', 'resize');
  353.  
  354. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  355. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  356. document.documentElement.removeEventListener("mouseup", stopResize, false);
  357. document.documentElement.removeEventListener("mouseup", stopMove, false);
  358.  
  359. document.documentElement.addEventListener("mousemove", resizeWindow);
  360. document.documentElement.addEventListener("mouseup", stopResize);
  361.  
  362. }
  363.  
  364.  
  365. let updateOpacityRid = 0;
  366.  
  367. function updateOpacity(chatWindow, rect, screen) {
  368.  
  369. let tid = ++updateOpacityRid;
  370.  
  371. requestAnimationFrame(() => {
  372.  
  373.  
  374. if (tid !== updateOpacityRid) return;
  375.  
  376. let { x, y, w, h } = rect;
  377. let [left, top, right, bottom] = [x, y, x + w, y + h];
  378.  
  379.  
  380. let opacityW = (Math.min(right, screen.width) - Math.max(0, left)) / w;
  381. let opacityH = (Math.min(bottom, screen.height) - Math.max(0, top)) / h;
  382.  
  383. let opacity = Math.min(opacityW, opacityH);
  384.  
  385. chatWindow.style.setProperty('--floating-window-opacity', Math.round(opacity * 100 * 5, 0) / 5 / 100);
  386.  
  387.  
  388. })
  389.  
  390.  
  391.  
  392.  
  393.  
  394. }
  395.  
  396. function initializeMove(e) {
  397.  
  398. if (!document.fullscreenElement) return;
  399.  
  400.  
  401.  
  402. const chatWindow = kRef(chatWindowWR);
  403.  
  404.  
  405.  
  406. startX = e.pageX;
  407. startY = e.pageY;
  408.  
  409.  
  410. if (chatWindow) {
  411.  
  412. Promise.resolve(chatWindow).then(chatWindow => {
  413.  
  414.  
  415. let rect = chatWindow.getBoundingClientRect();
  416. initialLeft = rect.x;
  417. initialTop = rect.y;
  418.  
  419.  
  420.  
  421. startWidth = rect.width;
  422. startHeight = rect.height;
  423.  
  424.  
  425. chatWindow.style.setProperty('--f3-left', initialLeft + "px");
  426. chatWindow.style.setProperty('--f3-top', initialTop + "px");
  427. chatWindow.style.setProperty('--f3-w', startWidth + "px");
  428. chatWindow.style.setProperty('--f3-h', startHeight + "px");
  429.  
  430. })
  431.  
  432.  
  433. }
  434.  
  435.  
  436.  
  437. document.documentElement.setAttribute('moving', 'move');
  438.  
  439. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  440. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  441. document.documentElement.removeEventListener("mouseup", stopResize, false);
  442. document.documentElement.removeEventListener("mouseup", stopMove, false);
  443.  
  444. document.documentElement.addEventListener("mousemove", moveWindow, false);
  445. document.documentElement.addEventListener("mouseup", stopMove, false);
  446.  
  447. e.stopPropagation();
  448. e.preventDefault();
  449.  
  450. beforeEvent = e;
  451.  
  452. }
  453.  
  454.  
  455. function checkClick(beforeEvent, currentEvent) {
  456.  
  457. if (currentEvent.timeStamp - beforeEvent.timeStamp < 300 && currentEvent.timeStamp - beforeEvent.timeStamp > 30) {
  458.  
  459. document.documentElement.classList.add('no-floating');
  460.  
  461. }
  462.  
  463. }
  464.  
  465.  
  466. stopResize = (e) => {
  467.  
  468. document.documentElement.removeAttribute('moving');
  469. document.documentElement.removeEventListener("mousemove", resizeWindow);
  470. }
  471.  
  472. stopMove = (e) => {
  473. document.documentElement.removeAttribute('moving');
  474. document.documentElement.removeEventListener("mousemove", moveWindow);
  475.  
  476. checkClick(beforeEvent, e)
  477. beforeEvent = null;
  478. }
  479.  
  480.  
  481. function reset() {
  482.  
  483. document.documentElement.removeAttribute('moving');
  484. document.documentElement.removeEventListener("mousemove", resizeWindow, false);
  485. document.documentElement.removeEventListener("mousemove", moveWindow, false);
  486. document.documentElement.removeEventListener("mouseup", stopResize, false);
  487. document.documentElement.removeEventListener("mouseup", stopMove, false);
  488.  
  489.  
  490. startX = 0;
  491. startY = 0;
  492. startWidth = 0;
  493. startHeight = 0;
  494.  
  495.  
  496. edge = 0;
  497.  
  498.  
  499. initialLeft = 0;
  500. initialTop = 0;
  501.  
  502. beforeEvent = null;
  503.  
  504.  
  505. }
  506.  
  507. function setChat(chat) {
  508.  
  509. let resizeHandle = HTMLElement.prototype.querySelector.call(chat, '.resize-handle')
  510. if (resizeHandle) return;
  511.  
  512.  
  513. 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;
  514. if (!script) addCSS();
  515.  
  516. if (!document.documentElement.hasAttribute('floating-chat-window')) document.documentElement.setAttribute('floating-chat-window', '');
  517.  
  518.  
  519. resizeHandle = document.createElement("div");
  520. resizeHandle.className = "resize-handle";
  521. chat.appendChild(resizeHandle);
  522. resizeHandle = null;
  523.  
  524. let chatWindow;
  525. let showHideButton;
  526.  
  527. chatWindow = kRef(chatWindowWR);
  528. showHideButton = kRef(showHideButtonWR);
  529.  
  530.  
  531.  
  532. if (chatWindow) chatWindow.removeEventListener("mousedown", initializeResize, false);
  533. if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);
  534.  
  535.  
  536. chatWindow = chat;
  537. showHideButton = HTMLElement.prototype.querySelector.call(chat, '#show-hide-button');
  538. chatWindowWR = mWeakRef(chat)
  539. showHideButtonWR = mWeakRef(showHideButton);
  540.  
  541.  
  542.  
  543. chatWindow.addEventListener("mousedown", initializeResize, false);
  544. showHideButton.addEventListener("mousedown", initializeMove, false);
  545.  
  546. reset();
  547.  
  548. }
  549.  
  550. function noChat(chat) {
  551.  
  552. let chatWindow;
  553. let showHideButton;
  554.  
  555. chatWindow = kRef(chatWindowWR);
  556. showHideButton = kRef(showHideButtonWR);
  557.  
  558.  
  559.  
  560. if (chatWindow) chatWindow.removeEventListener("mousedown", initializeResize, false);
  561. if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);
  562.  
  563.  
  564. let resizeHandle = HTMLElement.prototype.querySelector.call(chat, '.resize-handle')
  565. if (resizeHandle) {
  566. resizeHandle.remove();
  567. }
  568.  
  569. chat.removeEventListener("mousedown", initializeResize, false);
  570.  
  571.  
  572. showHideButton = HTMLElement.prototype.querySelector.call(chat, '#show-hide-button');
  573.  
  574. if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);
  575.  
  576.  
  577. reset();
  578. }
  579.  
  580. document.addEventListener('fullscreenchange', () => {
  581. document.documentElement.classList.remove('no-floating')
  582. })
  583.  
  584. customYtElements.whenRegistered('ytd-live-chat-frame', (proto) => {
  585.  
  586.  
  587. proto.attached = ((attached) => (function () { Promise.resolve(this).then(setChat); return attached.apply(this, arguments) }))(proto.attached);
  588.  
  589. proto.detached = ((detached) => (function () { Promise.resolve(this).then(noChat); return detached.apply(this, arguments) }))(proto.detached);
  590.  
  591. let chat = document.querySelector('ytd-live-chat-frame');
  592. if (chat) Promise.resolve(chat).then(setChat);
  593.  
  594. })
  595.  
  596.  
  597. })({ Promise, requestAnimationFrame });