YouTube: Floating Chat Window on Fullscreen

To make floating chat window on fullscreen

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

// ==UserScript==
// @name        YouTube: Floating Chat Window on Fullscreen
// @namespace   UserScript
// @match       https://www.youtube.com/*
// @exclude             /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
// @version     0.2.3
// @license             MIT License
// @author              CY Fung
// @description To make floating chat window on fullscreen
// @require             https://gf.qytechs.cn/scripts/465819-api-for-customelements-in-youtube/code/API%20for%20CustomElements%20in%20YouTube.js?version=1215161
// @run-at              document-start
// @grant               none
// @unwrap
// @allFrames           true
// @inject-into         page
// ==/UserScript==


((__CONTEXT__) => {

  const hkey_script = 'vdnvorrwsksy';


  const { isIframe, isTopFrame } = (() => {

    let isIframe = false, isTopFrame = false;
    try {
      isIframe = window.document !== top.document
    } catch (e) { }

    try {
      isTopFrame = window.document === top.document
    } catch (e) { }

    return { isIframe, isTopFrame };

  })();

  if (isIframe ^ isTopFrame) { } else return;

  if (isTopFrame) {



    const createStyleText = () => `


[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) {
  position:fixed !important;
    top: var(--f3-top, 5px) !important;
    left: var(--f3-left, calc(60vw + 100px)) !important;
    height: var(--f3-h, 60vh) !important;
    width: var(--f3-w, 320px) !important;
    display:flex !important;
    flex-direction: column !important;
    padding: 4px;
    cursor: all-scroll;
    z-index:9999;
    box-sizing: border-box !important;
    margin:0 !important;
    opacity: var(--floating-window-opacity, 1.0) !important;
    background: transparent;
    background-color: rgba(0, 0, 0, 0.5);
    transition: background-color 300ms;
}

[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]):hover {
    background-color: rgba(0, 0, 0, 0.85);

}

.no-floating[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) {

    top: -300vh !important;
    left: -300vh !important;
}


[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) #show-hide-button[class]{
    flex-grow: 0;
    flex-shrink:0;
    position:static;
    cursor: all-scroll;
}
[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) #show-hide-button[class] *[class]{
    cursor: inherit;
}
[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]) iframe[class]{
    flex-grow: 100;
    flex-shrink:0;
    height: 0;
    position:static;
}


html{
    --fc7-handle-color: #0cb8da;
}
html[dark]{
    --fc7-handle-color: #0c74e4;
}

:fullscreen .resize-handle {

    position: absolute !important;
    top: 0;
    left: 0;
    bottom: 0;
    background: transparent;
    right: 0;
    z-index: 999 !important;
    border-radius: inherit !important;
    box-sizing: border-box !important;
    pointer-events:none !important;
    visibility: collapse;
    border: 4px solid transparent;
    border-color: transparent;
    transition: border-color 300ms;
}

[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed]):hover .resize-handle {

    visibility: visible;

    border-color: var(--fc7-handle-color);
}

[moving] {
    cursor: all-scroll;
    --pointer-events:initial;
}

[moving] body {
    --pointer-events:none;
}

[moving] ytd-live-chat-frame#chat{

    --pointer-events:initial;
}


[moving] ytd-live-chat-frame#chat iframe {

    --pointer-events:none;
}


[moving="move"]  ytd-live-chat-frame#chat {
    background-color: var(--yt-spec-general-background-a);

}


[moving="move"] ytd-live-chat-frame#chat iframe {

    visibility: collapse;
}

[moving] * {
    pointer-events:var(--pointer-events) !important;

}
[moving] *, [moving] [class] {
    user-select: none !important;
}

:fullscreen tyt-iframe-popup-btn{
    display: none !important;
}

[moving] tyt-iframe-popup-btn{
    display: none !important;
}

[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 {

  background: transparent;

}



  `;

    const addCSS = () => {
      let text = createStyleText();
      let style = document.createElement('style');
      style.id = 'rvZ0t';
      style.textContent = text;
      document.head.appendChild(style);
    }

    const { Promise, requestAnimationFrame } = __CONTEXT__;

    let chatWindowWR = null;
    let showHideButtonWR = null;


    /* globals WeakRef:false */

    /** @type {(o: Object | null) => WeakRef | null} */
    const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'

    /** @type {(wr: Object | null) => Object | null} */
    const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);



    let startX;
    let startY;
    let startWidth;
    let startHeight;


    let edge = 0;


    let initialLeft;
    let initialTop;

    let stopResize;
    let stopMove;


    const getXY = (e) => {
      let rect = e.target.getBoundingClientRect();
      let x = e.clientX - rect.left; //x position within the element.
      let y = e.clientY - rect.top;  //y position within the element.
      return { x, y }
    }

    let beforeEvent = null;

    function resizeWindow(e) {


      const chatWindow = kRef(chatWindowWR);
      if (chatWindow) {

        const mEdge = edge;

        if (mEdge == 4 || mEdge == 1) {

        } else if (mEdge == 8 || mEdge == 16) {
        } else {
          return;
        }


        Promise.resolve(chatWindow).then(chatWindow => {
          let rect;

          if (mEdge == 4 || mEdge == 1 || mEdge == 16) {

            let newWidth = startWidth + (startX - e.pageX);

            let newLeft = initialLeft + startWidth - newWidth;
            chatWindow.style.setProperty('--f3-w', newWidth + "px");
            chatWindow.style.setProperty('--f3-left', newLeft + "px");



            let newHeight = startHeight + (startY - e.pageY);

            let newTop = initialTop + startHeight - newHeight;
            chatWindow.style.setProperty('--f3-h', newHeight + "px");
            chatWindow.style.setProperty('--f3-top', newTop + "px");

            rect = {
              x: newLeft,
              y: newTop,
              w: newWidth,

              h: newHeight,


            };



          } else if (mEdge == 8) {

            let newWidth = startWidth + e.pageX - startX;
            let newHeight = startHeight + e.pageY - startY;

            chatWindow.style.setProperty('--f3-w', newWidth + "px");
            chatWindow.style.setProperty('--f3-h', newHeight + "px");


            rect = {
              x: initialLeft,
              y: initialTop,
              w: newWidth,

              h: newHeight,


            };

          }



          updateOpacity(chatWindow, rect, screen);

        })


        e.stopPropagation();
        e.preventDefault();


      }

    }

    function moveWindow(e) {


      const chatWindow = kRef(chatWindowWR);
      if (chatWindow) {

        Promise.resolve(chatWindow).then(chatWindow => {


          let newX = initialLeft + e.pageX - startX;
          let newY = initialTop + e.pageY - startY;

          chatWindow.style.setProperty('--f3-left', newX + "px");
          chatWindow.style.setProperty('--f3-top', newY + "px");



          updateOpacity(chatWindow, {
            x: newX,
            y: newY,
            w: startWidth,

            h: startHeight,


          }, screen);

        });



        e.stopPropagation();
        e.preventDefault();

      }
    }




    function initializeResize(e) {

      if (!document.fullscreenElement) return;

      if (!document.querySelector('[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed])')) return;

      if (e.target.id !== 'chat') return;



      const { x, y } = getXY(e);
      edge = 0;
      if (x < 16 && y < 16) { edge = 16; }
      else if (x < 16) edge = 4;
      else if (y < 16) edge = 1;
      else edge = 8;

      if (edge <= 0) return;

      startX = e.pageX;
      startY = e.pageY;

      const chatWindow = kRef(chatWindowWR);
      if (chatWindow) {

        Promise.resolve(chatWindow).then(chatWindow => {

          let rect = chatWindow.getBoundingClientRect();
          initialLeft = rect.x;
          initialTop = rect.y;



          startWidth = rect.width;
          startHeight = rect.height;


          chatWindow.style.setProperty('--f3-left', initialLeft + "px");
          chatWindow.style.setProperty('--f3-top', initialTop + "px");
          chatWindow.style.setProperty('--f3-w', startWidth + "px");
          chatWindow.style.setProperty('--f3-h', startHeight + "px");

        });

      }




      document.documentElement.setAttribute('moving', 'resize');

      document.documentElement.removeEventListener("mousemove", resizeWindow, false);
      document.documentElement.removeEventListener("mousemove", moveWindow, false);
      document.documentElement.removeEventListener("mouseup", stopResize, false);
      document.documentElement.removeEventListener("mouseup", stopMove, false);

      document.documentElement.addEventListener("mousemove", resizeWindow);
      document.documentElement.addEventListener("mouseup", stopResize);

    }


    let updateOpacityRid = 0;

    function updateOpacity(chatWindow, rect, screen) {

      let tid = ++updateOpacityRid;

      requestAnimationFrame(() => {


        if (tid !== updateOpacityRid) return;

        let { x, y, w, h } = rect;
        let [left, top, right, bottom] = [x, y, x + w, y + h];


        let opacityW = (Math.min(right, screen.width) - Math.max(0, left)) / w;
        let opacityH = (Math.min(bottom, screen.height) - Math.max(0, top)) / h;

        let opacity = Math.min(opacityW, opacityH);

        chatWindow.style.setProperty('--floating-window-opacity', Math.round(opacity * 100 * 5, 0) / 5 / 100);


      })





    }

    function initializeMove(e) {

      if (!document.fullscreenElement) return;
      if (!document.querySelector('[floating-chat-window]:fullscreen ytd-live-chat-frame#chat:not([collapsed])')) return;



      const chatWindow = kRef(chatWindowWR);



      startX = e.pageX;
      startY = e.pageY;


      if (chatWindow) {

        Promise.resolve(chatWindow).then(chatWindow => {


          let rect = chatWindow.getBoundingClientRect();
          initialLeft = rect.x;
          initialTop = rect.y;



          startWidth = rect.width;
          startHeight = rect.height;


          chatWindow.style.setProperty('--f3-left', initialLeft + "px");
          chatWindow.style.setProperty('--f3-top', initialTop + "px");
          chatWindow.style.setProperty('--f3-w', startWidth + "px");
          chatWindow.style.setProperty('--f3-h', startHeight + "px");

        })


      }



      document.documentElement.setAttribute('moving', 'move');

      document.documentElement.removeEventListener("mousemove", resizeWindow, false);
      document.documentElement.removeEventListener("mousemove", moveWindow, false);
      document.documentElement.removeEventListener("mouseup", stopResize, false);
      document.documentElement.removeEventListener("mouseup", stopMove, false);

      document.documentElement.addEventListener("mousemove", moveWindow, false);
      document.documentElement.addEventListener("mouseup", stopMove, false);

      e.stopPropagation();
      e.preventDefault();

      beforeEvent = e;

    }


    function checkClick(beforeEvent, currentEvent) {

      const d = currentEvent.timeStamp - beforeEvent.timeStamp;
      if (d < 300 && d > 30) {

        document.documentElement.classList.add('no-floating');

      }

    }


    stopResize = (e) => {

      document.documentElement.removeAttribute('moving');
      document.documentElement.removeEventListener("mousemove", resizeWindow);
    }

    stopMove = (e) => {
      document.documentElement.removeAttribute('moving');
      document.documentElement.removeEventListener("mousemove", moveWindow);

      beforeEvent && checkClick(beforeEvent, e);
      beforeEvent = null;
    }


    function reset() {

      document.documentElement.removeAttribute('moving');
      document.documentElement.removeEventListener("mousemove", resizeWindow, false);
      document.documentElement.removeEventListener("mousemove", moveWindow, false);
      document.documentElement.removeEventListener("mouseup", stopResize, false);
      document.documentElement.removeEventListener("mouseup", stopMove, false);


      startX = 0;
      startY = 0;
      startWidth = 0;
      startHeight = 0;


      edge = 0;


      initialLeft = 0;
      initialTop = 0;

      beforeEvent = null;


    }

    function iframeLoaded() {

    }

    function iframeFullscreenChanged() {
      const iframeDoc = this;


      if (!document.fullscreenElement) {
        iframeDoc.documentElement.classList.remove('youtube-floating-chat-iframe');
      } else {
        iframeDoc.documentElement.classList.add('youtube-floating-chat-iframe');

      }


    }

    let iframeFullscreenChangedBinded = null;



    function onMessage(evt) {
      if (evt.data === 'vdnvorrwsksy') {

        const iframeWin = evt.source;
        const iframeDoc = iframeWin.document;



        function onReady() {

          iframeDoc.head.appendChild(document.createElement('style')).textContent = `

        .youtube-floating-chat-iframe #right-arrow-container.yt-live-chat-ticker-renderer,
        .youtube-floating-chat-iframe #left-arrow-container.yt-live-chat-ticker-renderer

        {
          background: transparent;
        }

.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app {

  --yt-live-chat-background-color: transparent;
  --yt-live-chat-action-panel-background-color: rgba(0, 0, 0, 0.08);
  --yt-live-chat-header-background-color: rgba(0, 0, 0, 0.18);
  --yt-spec-static-overlay-background-medium: rgba(0, 0, 0, 0.08);
  --yt-live-chat-banner-gradient-scrim: transparent;

}




.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app #visible-banners > yt-live-chat-banner-renderer {
  --fc7-banner-opacity: 0.86;
}

.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app #visible-banners > yt-live-chat-banner-renderer[collapsed] {
  --fc7-banner-opacity: 0.66;
}


.youtube-floating-chat-iframe yt-live-chat-app:hover yt-live-chat-renderer.yt-live-chat-app #visible-banners > yt-live-chat-banner-renderer[class] {
  --fc7-banner-opacity: 1;
}


.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app {
  --fc7-system-message-opacity: 0.66;

}

.youtube-floating-chat-iframe yt-live-chat-app:hover yt-live-chat-renderer.yt-live-chat-app {
  --fc7-system-message-opacity: 1.0;

}


.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app #visible-banners > yt-live-chat-banner-renderer {
  opacity: var(--fc7-banner-opacity) !important;
}


.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app yt-live-chat-viewer-engagement-message-renderer {
  opacity: var(--fc7-system-message-opacity) !important;
}


.youtube-floating-chat-iframe yt-live-chat-app:not(:hover) yt-live-chat-renderer.yt-live-chat-app yt-live-chat-message-input-renderer {
 display: none;
}

.youtube-floating-chat-iframe yt-live-chat-app:hover yt-live-chat-renderer.yt-live-chat-app yt-live-chat-message-input-renderer {

    position: absolute;
    transform: translateY(-100%);
    left: 0;
    right: 0;
    opacity: 1;
    background: rgba(0,0,0,0.86);
    }











[dark].youtube-floating-chat-iframe yt-live-chat-app ::-webkit-scrollbar-track,
[dark].youtube-floating-chat-iframe yt-live-chat-kevlar-container ::-webkit-scrollbar-track {
    background-color: var(--ytd-searchbox-legacy-button-color);
}

.youtube-floating-chat-iframe yt-live-chat-app ::-webkit-scrollbar-track,
.youtube-floating-chat-iframe yt-live-chat-kevlar-container ::-webkit-scrollbar-track {
    background-color: #fcfcfc;
}


[dark].youtube-floating-chat-iframe yt-live-chat-app ::-webkit-scrollbar-thumb,
[dark].youtube-floating-chat-iframe yt-live-chat-kevlar-container ::-webkit-scrollbar-thumb{

    background-color: var(--ytd-searchbox-legacy-button-color);
    border: 2px solid var(--ytd-searchbox-legacy-button-color);

}


/* hide message */
.youtube-floating-chat-iframe yt-live-chat-app:not(:hover) > tp-yt-iron-dropdown.yt-live-chat-app yt-tooltip-renderer[slot="dropdown-content"][position-type="OPEN_POPUP_POSITION_TOP"].yt-live-chat-app {
  visibility: collapse;
}



        `;


          if (iframeFullscreenChangedBinded) document.removeEventListener('fullscreenchange', iframeFullscreenChangedBinded, false);
          iframeFullscreenChangedBinded = iframeFullscreenChanged.bind(iframeDoc);
          document.addEventListener('fullscreenchange', iframeFullscreenChangedBinded, false);

          iframeFullscreenChangedBinded();


        }



        Promise.resolve().then(() => {

          if (iframeDoc.readyState !== 'loading') {
            onReady();
          } else {
            iframeWin.addEventListener("DOMContentLoaded", onReady, false);
          }

        });


      }

    }


    function setChat(chat) {

      let resizeHandle = HTMLElement.prototype.querySelector.call(chat, '.resize-handle')
      if (resizeHandle) return;



      let cw = (() => {
        try {
          const { head, body } = chat.$.chatframe.contentWindow.document;
          return { head, body }

        } catch (e) { return null; }
      })();

      if (!cw) return;

      window.removeEventListener('message', onMessage, false);
      window.addEventListener('message', onMessage, false);



      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;
      if (!script) addCSS();

      if (!document.documentElement.hasAttribute('floating-chat-window')) document.documentElement.setAttribute('floating-chat-window', '');


      chat.setAttribute('allowtransparency', 'true');



      resizeHandle = document.createElement("div");
      resizeHandle.className = "resize-handle";
      chat.appendChild(resizeHandle);
      resizeHandle = null;

      let chatWindow;
      let showHideButton;

      chatWindow = kRef(chatWindowWR);
      showHideButton = kRef(showHideButtonWR);



      if (chatWindow) chatWindow.removeEventListener("mousedown", initializeResize, false);
      if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);


      chatWindow = chat;
      showHideButton = HTMLElement.prototype.querySelector.call(chat, '#show-hide-button');
      chatWindowWR = mWeakRef(chat)
      showHideButtonWR = mWeakRef(showHideButton);



      chatWindow.addEventListener("mousedown", initializeResize, false);
      showHideButton.addEventListener("mousedown", initializeMove, false);

      reset();

    }


    const fullscreenchangePageFn = () => {
      if (!document.fullscreenElement) {
        document.documentElement.classList.remove('no-floating')
      }
    };

    function noChat(chat) {

      let chatWindow;
      let showHideButton;

      chatWindow = kRef(chatWindowWR);
      showHideButton = kRef(showHideButtonWR);



      if (chatWindow) chatWindow.removeEventListener("mousedown", initializeResize, false);
      if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);


      let resizeHandle = HTMLElement.prototype.querySelector.call(chat, '.resize-handle')
      if (resizeHandle) {
        resizeHandle.remove();
      }

      chat.removeEventListener("mousedown", initializeResize, false);


      showHideButton = HTMLElement.prototype.querySelector.call(chat, '#show-hide-button');

      if (showHideButton) showHideButton.removeEventListener("mousedown", initializeMove, false);


      reset();
    }


    document.removeEventListener('fullscreenchange', fullscreenchangePageFn, false);
    document.addEventListener('fullscreenchange', fullscreenchangePageFn, false);
    fullscreenchangePageFn();

    customYtElements.whenRegistered('ytd-live-chat-frame', (proto) => {


      proto.attached = ((attached) => (function () { Promise.resolve(this).then(setChat); return attached.apply(this, arguments) }))(proto.attached);

      proto.detached = ((detached) => (function () { Promise.resolve(this).then(noChat); return detached.apply(this, arguments) }))(proto.detached);

      let chat = document.querySelector('ytd-live-chat-frame');
      if (chat) Promise.resolve(chat).then(setChat);

    })




  } else if (isIframe && top === parent) {



    top.postMessage(hkey_script, `${location.protocol}//${location.hostname}`);




  }




})({ Promise, requestAnimationFrame });

QingJ © 2025

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