// ==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.1.4 // @license MIT License // @author CY Fung // @description To make floating chat window on fullscreen // @require https://greasyfork.org/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 // @downloadURL none // ==/UserScript== ((__CONTEXT__) => { const createStyleText = () => ` :fullscreen ytd-live-chat-frame#chat { 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; } .no-floating:fullscreen ytd-live-chat-frame#chat { top: -300vh !important; left: -300vh !important; } :fullscreen ytd-live-chat-frame#chat #show-hide-button[class]{ flex-grow: 0; flex-shrink:0; position:static; cursor: all-scroll; } :fullscreen ytd-live-chat-frame#chat #show-hide-button[class] *[class]{ cursor: inherit; } :fullscreen ytd-live-chat-frame#chat iframe[class]{ flex-grow: 100; flex-shrink:0; height: 0; position:static; } html{ --f7-handle-color: #0cb8da; } html[dark]{ --f7-handle-color: #0c74e4; } :fullscreen .resize-handle{ position: absolute; top: 0; left: 0; bottom: 0; background: transparent; right: 0; border: 4px solid var(--f7-handle-color); z-index: 999; border-radius: inherit; box-sizing: border-box; pointer-events:none; } [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; } :fullscreen tyt-iframe-popup-btn{ display: none !important; } [moving] tyt-iframe-popup-btn{ display: none !important; } `; 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) { } else { return; } Promise.resolve(chatWindow).then(chatWindow => { let rect; if (mEdge == 4 || mEdge == 1) { 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 (e.target.id !== 'chat') return; const { x, y } = getXY(e); edge = 0; if (x < 16 && y < 16) { edge = -1 } 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; 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) { if (currentEvent.timeStamp - beforeEvent.timeStamp < 300 && currentEvent.timeStamp - beforeEvent.timeStamp > 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); 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 setChat(chat) { let resizeHandle = HTMLElement.prototype.querySelector.call(chat, '.resize-handle') if (resizeHandle) return; 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(); 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); }) })({ Promise, requestAnimationFrame });