// ==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.0
// @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;
console.log(123)
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;
console.log(122)
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 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 yt-live-chat-banner-manager #visible-banners > yt-live-chat-banner-renderer {
--fc7-banner-opacity: 0.86;
}
.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app yt-live-chat-banner-manager #visible-banners > yt-live-chat-banner-renderer[collapsed] {
--fc7-banner-opacity: 0.66;
}
.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app yt-live-chat-banner-manager:hover #visible-banners > yt-live-chat-banner-renderer[class] {
--fc7-banner-opacity: 1;
}
.youtube-floating-chat-iframe yt-live-chat-renderer.yt-live-chat-app yt-live-chat-banner-manager #visible-banners > yt-live-chat-banner-renderer {
opacity: var(--fc7-banner-opacity) !important;
}
[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);
}
`;
iframeDoc.documentElement.classList.add('youtube-floating-chat-iframe');
}
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();
}
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.addEventListener('fullscreenchange', () => {
document.documentElement.classList.remove('no-floating')
})
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 });