您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
让所有视频网页全屏,开启画中画功能,支持自定义按钮位置。
// ==UserScript== // @name 视频网页全屏(改) // @name:en Maximize Video(Modify) // @name:zh-CN 视频网页全屏(改) // @name:zh-TW 視頻網頁全屏(改) // @name:ja ビデオページ全画面(変更) // @name:ko 비디오 웹페이지 전체화면(수정) // @namespace https://greasyfork.org/zh-CN/users/178351-yesilin // @description 让所有视频网页全屏,开启画中画功能,支持自定义按钮位置。 // @description:en Maximize all video players.Support Piture-in-picture and custom button position. // @description:zh-CN 让所有视频网页全屏,开启画中画功能,支持自定义按钮位置。 // @description:zh-TW 讓所有視頻網頁全屏,開啟畫中畫功能,支援自定義按鈕位置。 // @description:ja すべての動画ページを全画面表示し、ピクチャ・イン・ピクチャ機能を有効にします。ボタン位置のカスタマイズにも対応しています。 // @description:ko 모든 비디오 웹페이지를 전체화면으로 전환하고, PIP(화면 속 화면) 기능과 사용자 지정 버튼 위치를 지원합니다. // @author 冻猫, RyomaHan, YeSilin // @include * // @exclude *www.w3school.com.cn* // @version 12.5.23 // @run-at document-end // @license MIT // @grant GM_setValue // @grant GM_getValue // @icon data:image/svg+xml;base64,PHN2ZyBpZD0i5Zu+5bGCXzEiIGRhdGEtbmFtZT0i5Zu+5bGCIDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIwMCAyMDAiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojMDQzNDQzO30uY2xzLTJ7ZmlsbDojMjdjNmZmO308L3N0eWxlPjwvZGVmcz48ZyBpZD0i5Zu+5bGCXzIiIGRhdGEtbmFtZT0i5Zu+5bGCIDIiPjxjaXJjbGUgY2xhc3M9ImNscy0xIiBjeD0iMTAwIiBjeT0iMTAwIiByPSIxMDAiLz48L2c+PGcgaWQ9IuWbvuWxgl8xLTIiIGRhdGEtbmFtZT0i5Zu+5bGCIDEiPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEwMC43NCwxMTEuMThzNDguOTItLjQ1LDU2LjM1LDM1LjE5bC0xNCw0LDIuMzMsMTUuMzFhMzguOTIsMzguOTIsMCwwLDEtMTcuNjMtLjlBNDEsNDEsMCwwLDEsMTAwLjA2LDExNGExOS40OCwxOS40OCwwLDAsMSwuNi0xLjg4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEwNS42OSwxMDAuMDhzMjMuOTQtNDIuNTUsNTguNTMtMzEuMTVsLTMuNjgsMTRMMTc1LDg4LjU5YTM5Ljc2LDM5Ljc2LDAsMCwxLTkuNiwxNC44Niw0MC44Miw0MC44MiwwLDAsMS01Ny43OC0xLjEyYy0uNTMtLjUzLTEtMS0xLjQzLTEuNThaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNOTcuNjYsOTYuN1M2Mi43Nyw2Mi41Niw4Mi44LDMyLjA5bDEyLjYxLDcuMTMsOS4zLTEyLjUzYTM5LDM5LDAsMCwxLDExLjk0LDEzLjA1LDQxLjA2LDQxLjA2LDAsMCwxLTE2LjE0LDU1LjY4bC0xLjguOVoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik05MS4yMSwxMDIuNDhzLTQyLDI0Ljc2LTY2LjI2LTIuNEwzNS4wOCw4OS42NCwyNS4zMyw3Ny43MUEzOSwzOSwwLDAsMSw0MSw2OS43Niw0MC45LDQwLjksMCwwLDEsOTAuNTMsOTkuN2MuMTUuNjguMywxLjM1LjQ1LDJaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNOTIuNzgsMTA5LjlzMTMuMTQsNDctMTkuMzYsNjMuNDFsLTcuMzUtMTIuMzgtMTQuMjYsNi4yM2EzOCwzOCwwLDAsMS0zLjY3LTE3LjI2LDQwLjksNDAuOSwwLDAsMSw0MS43OS00MGMuNjgsMCwxLjQzLjA4LDIuMS4wOFoiLz48L2c+PC9zdmc+ // ==/UserScript== (() => { // 全局变量存储对象 (global variables) const gv = { isFull: false, // 是否处于全屏状态 isIframe: false, // 当前页面是否在iframe中 autoCheckCount: 0, // 自动检测计数器 btnText: {}, // 按钮文本(多语言支持) player: null, // 当前激活的视频播放器元素 playerChilds: [], // 播放器子元素列表 playerParents: [], // 播放器父元素列表 backHtmlId: "", // 全屏前html元素的id backBodyId: "", // 全屏前body元素的id backControls: null, // 全屏前视频控件状态 ytbStageChange: false, // YouTube舞台模式切换标记 scrollFixTimer: null, // 滚动修正定时器 mouseoverEl: null, // 鼠标悬停元素 btnPosition: GM_getValue("buttonPosition", "top-right"), // 按钮位置,默认右上角 // 按钮元素 picinpicBtn: null, controlBtn: null, leftBtn: null, rightBtn: null, contextMenu: null, // 右键菜单元素 }; // Html5播放器规则[播放器最外层],适用于无法自动识别的自适应大小HTML5播放器 // 键为域名,值为该域名下播放器元素的选择器数组 const html5Rules = { "www.acfun.cn": [".player-container .player"], "www.bilibili.com": ["#bilibiliPlayer"], "www.douyu.com": ["#js-player-video-case"], "www.huya.com": ["#videoContainer"], "www.twitch.tv": [".player"], "www.youtube.com": ["#ytd-player"], "www.miguvideo.com": ["#mod-player"], "www.yy.com": ["#player"], "*weibo.com": ['[aria-label="Video Player"]', ".html5-video-live .html5-video"], "v.huya.com": ["#video_embed_flash>div"], "v.qq.com": ["#player-container"], }; // 通用html5播放器选择器,用于匹配常见的视频播放器类名 const generalPlayerRules = [".dplayer", ".video-js", ".jwplayer", "[data-player]"]; // 判断当前页面是否在iframe中 if (window.top !== window.self) { gv.isIframe = true; } // 根据浏览器语言设置按钮文本 if (navigator.language.toLocaleLowerCase() == "zh-cn") { gv.btnText = { max: "网页全屏", maxTooltip: "切换网页全屏(ESC),右键选择按钮位置", // 悬浮提示 pip: "画中画", pipTooltip: "切换画中画(F2),右键选择按钮位置", // 悬浮提示 tip: "Iframe内视频,请用鼠标点击视频后重试", menuTitle: "选择按钮位置", topLeft: "左上角", topRight: "右上角", }; } else { gv.btnText = { max: "Maximize", maxTooltip: "Toggle fullscreen (ESC). Right-click to choose button position", // 悬浮提示 pip: "PicInPic", pipTooltip: "Toggle Picture-in-Picture (F2). Right-click to choose button position", // 悬浮提示 tip: "Iframe video. Please click on the video and try again", menuTitle: "Choose Button Position", topLeft: "Top Left", topRight: "Top Right", }; } // 工具函数集合 const tool = { /** * 带时间戳的日志打印 * @param {string} log - 日志内容 */ print(log) { const now = new Date(); const year = now.getFullYear(); const month = (now.getMonth() + 1 < 10 ? "0" : "") + (now.getMonth() + 1); const day = (now.getDate() < 10 ? "0" : "") + now.getDate(); const hour = (now.getHours() < 10 ? "0" : "") + now.getHours(); const minute = (now.getMinutes() < 10 ? "0" : "") + now.getMinutes(); const second = (now.getSeconds() < 10 ? "0" : "") + now.getSeconds(); const timenow = "[" + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second + "]"; console.log(timenow + "[Maximize Video] > " + log); }, /** * 获取元素的位置信息 * @param {HTMLElement} element - 目标元素 * @returns {Object} 包含页面坐标和屏幕坐标的对象 */ getRect(element) { const rect = element.getBoundingClientRect(); const scroll = tool.getScroll(); return { pageX: rect.left + scroll.left, // 元素左上角在页面中的X坐标 pageY: rect.top + scroll.top, // 元素左上角在页面中的Y坐标 screenX: rect.left, // 元素左上角在视口中的X坐标 screenY: rect.top, // 元素左上角在视口中的Y坐标 width: rect.width, height: rect.height, }; }, /** * 判断元素是否半全屏显示(宽或高接近视口) * @param {HTMLElement} element - 目标元素 * @returns {boolean} 是否半全屏 */ isHalfFullClient(element) { const client = tool.getClient(); const rect = tool.getRect(element); // 宽或高接近视口,且元素居中显示 if ( (Math.abs(client.width - element.offsetWidth) < 21 && rect.screenX < 20) || (Math.abs(client.height - element.offsetHeight) < 21 && rect.screenY < 10) ) { if ( Math.abs(element.offsetWidth / 2 + rect.screenX - client.width / 2) < 21 && Math.abs(element.offsetHeight / 2 + rect.screenY - client.height / 2) < 21 ) { return true; } else { return false; } } else { return false; } }, /** * 判断元素是否完全全屏显示(宽和高都接近视口) * @param {HTMLElement} element - 目标元素 * @returns {boolean} 是否完全全屏 */ isAllFullClient(element) { const client = tool.getClient(); const rect = tool.getRect(element); // 宽和高都接近视口,且位置在左上角附近 if ( Math.abs(client.width - element.offsetWidth) < 21 && rect.screenX < 20 && Math.abs(client.height - element.offsetHeight) < 21 && rect.screenY < 10 ) { return true; } else { return false; } }, /** * 获取页面滚动距离 * @returns {Object} 包含左右和上下滚动距离的对象 */ getScroll() { return { left: document.documentElement.scrollLeft || document.body.scrollLeft, top: document.documentElement.scrollTop || document.body.scrollTop, }; }, /** * 获取视口尺寸 * @returns {Object} 包含视口宽高的对象 */ getClient() { return { width: document.compatMode == "CSS1Compat" ? document.documentElement.clientWidth : document.body.clientWidth, height: document.compatMode == "CSS1Compat" ? document.documentElement.clientHeight : document.body.clientHeight, }; }, /** * 向页面添加CSS样式 * @param {string} css - CSS样式字符串 * @returns {HTMLElement} 创建的style元素 */ addStyle(css) { const style = document.createElement("style"); style.type = "text/css"; const node = document.createTextNode(css); style.appendChild(node); document.head.appendChild(style); return style; }, /** * 匹配字符串与规则(支持通配符*) * @param {string} str - 要匹配的字符串 * @param {string} rule - 包含*的规则字符串 * @returns {boolean} 是否匹配 */ matchRule(str, rule) { return new RegExp("^" + rule.split("*").join(".*") + "$").test(str); }, /** * 创建按钮元素 * @param {string} id - 按钮id * @returns {HTMLElement} 创建的按钮元素 */ createButton(id, title) { const btn = document.createElement("tbdiv"); btn.id = id; btn.title = title; // 设置提示文本 btn.onclick = () => { maximize.playerControl(); }; btn.addEventListener("contextmenu", handle.showContextMenu); document.body.appendChild(btn); return btn; }, /** * 显示提示信息 * @param {string} str - 提示文本 * @returns {Promise} 提示显示完成的Promise */ async addTip(str) { if (!document.getElementById("catTip")) { const tip = document.createElement("tbdiv"); tip.id = "catTip"; tip.innerHTML = str; tip.style.cssText = 'transition: all 0.8s ease-out;background: none repeat scroll 0 0 #27a9d8;color: #FFFFFF;font: 1.1em "微软雅黑";margin-left: -250px;overflow: hidden;padding: 10px;position: fixed;text-align: center;bottom: 100px;z-index: 300;'; document.body.appendChild(tip); tip.style.right = -tip.offsetWidth - 5 + "px"; // 显示提示动画 await new Promise((resolve) => { tip.style.display = "block"; setTimeout(() => { tip.style.right = "25px"; resolve("OK"); }, 300); }); // 停留一段时间 await new Promise((resolve) => { setTimeout(() => { tip.style.right = -tip.offsetWidth - 5 + "px"; resolve("OK"); }, 3500); }); // 移除提示元素 await new Promise((resolve) => { setTimeout(() => { document.body.removeChild(tip); resolve("OK"); }, 1000); }); } }, }; // 按钮设置相关方法 const setButton = { /** * 初始化按钮 */ init() { if (!document.getElementById("playerControlBtn")) { init(); } // 如果在iframe中且播放器半全屏,向父窗口发送消息 if (gv.isIframe && tool.isHalfFullClient(gv.player)) { window.parent.postMessage("iframeVideo", "*"); return; } this.show(); }, /** * 显示按钮并设置事件监听 */ show() { // 移除并重新添加鼠标离开事件监听,避免重复绑定 gv.player.removeEventListener("mouseleave", handle.leavePlayer, false); gv.player.addEventListener("mouseleave", handle.leavePlayer, false); // 非全屏状态下添加滚动监听,用于修正按钮位置 if (!gv.isFull) { document.removeEventListener("scroll", handle.scrollFix, false); document.addEventListener("scroll", handle.scrollFix, false); } gv.controlBtn.style.display = "block"; gv.controlBtn.style.visibility = "visible"; // 支持画中画功能且播放器不是OBJECT/EMBED时显示画中画按钮 if (document.pictureInPictureEnabled && gv.player.nodeName != "OBJECT" && gv.player.nodeName != "EMBED") { gv.picinpicBtn.style.display = "block"; gv.picinpicBtn.style.visibility = "visible"; } this.locate(); }, /** * 定位按钮位置(基于播放器位置和用户设置) */ locate() { let escapeHTMLPolicy; // 处理可信类型(Trusted Types)安全策略 const hasTrustedTypes = Boolean(window.trustedTypes && window.trustedTypes.createPolicy); if (hasTrustedTypes) { escapeHTMLPolicy = window.trustedTypes.createPolicy("myEscapePolicy", { createHTML: (string, sink) => string, }); } const playerRect = tool.getRect(gv.player); const client = tool.getClient(); // 设置按钮样式和位置(根据用户配置) gv.controlBtn.style.opacity = "0.5"; gv.controlBtn.innerHTML = hasTrustedTypes ? escapeHTMLPolicy.createHTML(gv.btnText.max) : gv.btnText.max; gv.picinpicBtn.style.opacity = "0.5"; gv.picinpicBtn.innerHTML = hasTrustedTypes ? escapeHTMLPolicy.createHTML(gv.btnText.pip) : gv.btnText.pip; // 根据用户设置的位置放置按钮 if (gv.btnPosition === "top-left") { // 左上角位置 gv.controlBtn.style.top = playerRect.screenY - gv.controlBtn.offsetHeight + "px"; gv.controlBtn.style.left = playerRect.screenX + "px"; gv.picinpicBtn.style.top = gv.controlBtn.style.top; gv.picinpicBtn.style.left = playerRect.screenX + gv.controlBtn.offsetWidth + 1 + "px"; } else { // 右上角位置(默认) gv.controlBtn.style.top = playerRect.screenY - gv.controlBtn.offsetHeight + "px"; gv.controlBtn.style.left = playerRect.screenX + gv.player.offsetWidth - gv.controlBtn.offsetWidth + "px"; gv.picinpicBtn.style.top = gv.controlBtn.style.top; gv.picinpicBtn.style.left = parseFloat(gv.controlBtn.style.left) - gv.picinpicBtn.offsetWidth - 1 + "px"; } }, }; // 事件处理相关方法 const handle = { /** * 获取鼠标悬停的播放器元素 * @param {MouseEvent} e - 鼠标事件对象 */ getPlayer(e) { if (gv.isFull) { return; } gv.mouseoverEl = e.target; const hostname = document.location.hostname; let players = []; // 1. 优先使用站点特定规则匹配播放器 for (let i in html5Rules) { if (tool.matchRule(hostname, i)) { for (let html5Rule of html5Rules[i]) { const elements = document.querySelectorAll(html5Rule); if (elements.length > 0) { players.push(...elements); // 直接扩展,避免二次查询 } } break; } } // 2. 站点规则未匹配到,使用通用规则 if (players.length == 0) { for (let generalPlayerRule of generalPlayerRules) { if (document.querySelectorAll(generalPlayerRule).length > 0) { for (let player of document.querySelectorAll(generalPlayerRule)) { players.push(player); } } } } // 3. 通用规则未匹配到,尝试直接匹配video元素 if (players.length == 0 && e.target.nodeName != "VIDEO" && document.querySelectorAll("video").length > 0) { const videos = document.querySelectorAll("video"); for (let v of videos) { const vRect = v.getBoundingClientRect(); // 匹配鼠标在视频区域内且尺寸足够大的视频 if ( e.clientX >= vRect.x - 2 && e.clientX <= vRect.x + vRect.width + 2 && e.clientY >= vRect.y - 2 && e.clientY <= vRect.y + vRect.height + 2 && v.offsetWidth > 399 && v.offsetHeight > 220 ) { players = []; players[0] = handle.autoCheck(v); gv.autoCheckCount = 1; break; } } } // 4. 从匹配到的播放器中找到鼠标所在的那个 if (players.length > 0) { const path = e.path || e.composedPath(); for (let v of players) { if (path.indexOf(v) > -1) { gv.player = v; setButton.init(); return; } } } // 5. 直接处理视频相关元素(VIDEO/OBJECT/EMBED) switch (e.target.nodeName) { case "VIDEO": case "OBJECT": case "EMBED": // 只处理尺寸足够大的播放器 if (e.target.offsetWidth > 399 && e.target.offsetHeight > 220) { gv.player = e.target; setButton.init(); } break; default: handle.leavePlayer(); } }, /** * 自动检测播放器的父容器(寻找与视频尺寸相近的容器) * @param {HTMLElement} v - 视频元素 * @returns {HTMLElement} 最合适的播放器容器 */ autoCheck(v) { let tempPlayer, el = v; gv.playerChilds = []; gv.playerChilds.push(v); // 向上遍历父节点,寻找与视频尺寸相近的容器 while ((el = el.parentNode)) { if (Math.abs(v.offsetWidth - el.offsetWidth) < 15 && Math.abs(v.offsetHeight - el.offsetHeight) < 15) { tempPlayer = el; gv.playerChilds.push(el); } else { break; } } return tempPlayer; }, /** * 处理鼠标离开播放器的事件(隐藏按钮) */ leavePlayer() { if (gv.controlBtn.style.visibility == "visible") { gv.controlBtn.style.opacity = ""; gv.controlBtn.style.visibility = ""; gv.picinpicBtn.style.opacity = ""; gv.picinpicBtn.style.visibility = ""; gv.player.removeEventListener("mouseleave", handle.leavePlayer, false); document.removeEventListener("scroll", handle.scrollFix, false); } }, /** * 处理滚动事件(延迟修正按钮位置,避免频繁触发) */ scrollFix(e) { clearTimeout(gv.scrollFixTimer); gv.scrollFixTimer = setTimeout(() => { setButton.locate(); }, 20); }, /** * 处理键盘快捷键 * @param {KeyboardEvent} e - 键盘事件对象 */ hotKey(e) { // ESC键:切换全屏状态(默认) if (e.keyCode == 27) { maximize.playerControl(); } // F2键:切换画中画(默认) if (e.keyCode == 113) { handle.pictureInPicture(); } }, /** * 处理鼠标中键点击事件 * @param {MouseEvent} e - 鼠标事件对象 */ mouseMiddleClick(e) { // 只在全屏状态下响应鼠标中键(button=1表示中键) if (gv.isFull && e.button === 1) { e.preventDefault(); // 阻止默认行为(如打开链接) maximize.playerControl(); // 退出全屏 } }, /** * 处理跨窗口消息 * @param {MessageEvent} e - 消息事件对象 */ async receiveMessage(e) { switch (e.data) { case "iframePicInPic": tool.print("messege:iframePicInPic"); // 处理iframe中的画中画请求 if (!document.pictureInPictureElement) { await document .querySelector("video") .requestPictureInPicture() .catch((error) => { tool.addTip(gv.btnText.tip); }); } else { await document.exitPictureInPicture(); } break; case "iframeVideo": tool.print("messege:iframeVideo"); // 处理iframe中的视频全屏请求 if (!gv.isFull) { gv.player = gv.mouseoverEl; setButton.init(); } break; case "parentFull": tool.print("messege:parentFull"); // 处理父窗口的全屏请求 gv.player = gv.mouseoverEl; if (gv.isIframe) { window.parent.postMessage("parentFull", "*"); } maximize.checkParent(); maximize.fullWin(); // 修正特定播放器的位置 if (getComputedStyle(gv.player).left != "0px") { tool.addStyle( "#htmlToothbrush #bodyToothbrush .playerToothbrush {left:0px !important;width:100vw !important;}" ); } gv.isFull = true; break; case "parentSmall": tool.print("messege:parentSmall"); // 处理父窗口的退出全屏请求 if (gv.isIframe) { window.parent.postMessage("parentSmall", "*"); } maximize.smallWin(); break; case "innerFull": tool.print("messege:innerFull"); // 处理iframe内部的全屏请求 if (gv.player.nodeName == "IFRAME") { gv.player.contentWindow.postMessage("innerFull", "*"); } maximize.checkParent(); maximize.fullWin(); break; case "innerSmall": tool.print("messege:innerSmall"); // 处理iframe内部的退出全屏请求 if (gv.player.nodeName == "IFRAME") { gv.player.contentWindow.postMessage("innerSmall", "*"); } maximize.smallWin(); break; } }, /** * 处理画中画功能切换 */ pictureInPicture() { if (!document.pictureInPictureElement) { if (gv.player) { if (gv.player.nodeName == "IFRAME") { // 向iframe发送画中画请求 gv.player.contentWindow.postMessage("iframePicInPic", "*"); } else { // 直接请求画中画 gv.player.parentNode.querySelector("video").requestPictureInPicture(); } } else { // 没有指定播放器时使用第一个video元素 document.querySelector("video").requestPictureInPicture(); } } else { // 退出画中画 document.exitPictureInPicture(); } }, /** * 显示右键菜单 * @param {MouseEvent} e - 鼠标事件对象 */ showContextMenu(e) { e.preventDefault(); // 先移除已存在的菜单 handle.hideContextMenu(); // 创建菜单元素 gv.contextMenu = document.createElement("div"); gv.contextMenu.id = "btnContextMenu"; // 添加菜单项 const menuTitle = document.createElement("div"); menuTitle.textContent = gv.btnText.menuTitle; gv.contextMenu.appendChild(menuTitle); // 左上角选项 const topLeftItem = document.createElement("div"); topLeftItem.textContent = gv.btnText.topLeft; topLeftItem.addEventListener("click", () => { handle.setButtonPosition("top-left"); handle.hideContextMenu(); }); gv.contextMenu.appendChild(topLeftItem); // 右上角选项 const topRightItem = document.createElement("div"); topRightItem.textContent = gv.btnText.topRight; topRightItem.addEventListener("click", () => { handle.setButtonPosition("top-right"); handle.hideContextMenu(); }); gv.contextMenu.appendChild(topRightItem); document.body.appendChild(gv.contextMenu); // 计算菜单位置,确保在视口内 const client = tool.getClient(); const menuRect = gv.contextMenu.getBoundingClientRect(); // 初始位置在鼠标下方 let left = e.clientX; let top = e.clientY; // 边界检测 - 水平方向 if (left + menuRect.width > client.width) { left = client.width - menuRect.width; } // 边界检测 - 垂直方向 if (top + menuRect.height > client.height) { top = client.height - menuRect.height; } // 应用位置 gv.contextMenu.style.left = left + "px"; gv.contextMenu.style.top = top + "px"; // 点击页面其他地方关闭菜单 document.addEventListener("click", handle.hideContextMenuOnce); }, /** * 隐藏右键菜单 */ hideContextMenu() { if (gv.contextMenu && gv.contextMenu.parentNode) { document.body.removeChild(gv.contextMenu); gv.contextMenu = null; } }, /** * 一次性隐藏菜单的事件处理 */ hideContextMenuOnce() { handle.hideContextMenu(); document.removeEventListener("click", handle.hideContextMenuOnce); }, /** * 设置按钮位置并保存 * @param {string} position - 位置值 ('top-left' 或 'top-right') */ setButtonPosition(position) { gv.btnPosition = position; GM_setValue("buttonPosition", position); setButton.locate(); // 重新定位按钮 }, }; // 网页全屏相关方法 const maximize = { /** * 播放器控制(切换全屏/退出全屏) */ playerControl() { if (!gv.player) { return; } this.checkParent(); if (!gv.isFull) { // 进入全屏 if (gv.isIframe) { window.parent.postMessage("parentFull", "*"); } if (gv.player.nodeName == "IFRAME") { gv.player.contentWindow.postMessage("innerFull", "*"); } this.fullWin(); // 自动调整播放器容器(最多尝试10次) if (gv.autoCheckCount > 0 && !tool.isHalfFullClient(gv.playerChilds[0])) { if (gv.autoCheckCount > 10) { for (let v of gv.playerChilds) { v.classList.add("videoToothbrush"); } return; } const tempPlayer = handle.autoCheck(gv.playerChilds[0]); gv.autoCheckCount++; maximize.playerControl(); gv.player = tempPlayer; maximize.playerControl(); } else { gv.autoCheckCount = 0; } } else { // 退出全屏 if (gv.isIframe) { window.parent.postMessage("parentSmall", "*"); } if (gv.player.nodeName == "IFRAME") { gv.player.contentWindow.postMessage("innerSmall", "*"); } this.smallWin(); } }, /** * 记录播放器的父元素链(用于退出全屏时恢复) */ checkParent() { if (gv.isFull) { return; } gv.playerParents = []; let full = gv.player; // 遍历父节点直到body while ((full = full.parentNode)) { if (full.nodeName == "BODY") { break; } if (full.getAttribute) { gv.playerParents.push(full); } } }, /** * 进入全屏状态 */ fullWin() { if (!gv.isFull) { // 移除鼠标悬停监听(全屏状态不需要) document.removeEventListener("mouseover", handle.getPlayer, false); // 保存原始id(用于恢复) gv.backHtmlId = document.body.parentNode.id; gv.backBodyId = document.body.id; // 显示辅助按钮 gv.leftBtn.style.display = "block"; gv.rightBtn.style.display = "block"; gv.picinpicBtn.style.display = ""; gv.controlBtn.style.display = ""; // 添加全屏相关样式类 this.addClass(); const hostname = document.location.hostname; // YouTube特殊处理:切换剧院模式 if (hostname.includes("www.youtube.com")) { const flexy = document.querySelector("#page-manager > ytd-watch-flexy"); // 是否处于剧院模式 const isTheaterMode = flexy && getComputedStyle(flexy).getPropertyValue("--ytd-watch-flexy-chat-max-height").trim() === "460px"; // 不是剧院模式就自动进入宽屏模式 if (!isTheaterMode) { document.querySelector("#movie_player .ytp-size-button").click(); gv.ytbStageChange = true; } } } gv.isFull = true; }, /** * 为全屏状态添加样式类 */ addClass() { // 修改根元素id(用于CSS选择器定位) document.body.parentNode.id = "htmlToothbrush"; document.body.id = "bodyToothbrush"; // 为父元素添加样式类 for (let v of gv.playerParents) { v.classList.add("parentToothbrush"); // 处理fixed定位的父元素(避免层级问题) if (getComputedStyle(v).position == "fixed") { v.classList.add("absoluteToothbrush"); } } // 为播放器添加样式类 gv.player.classList.add("playerToothbrush"); // 确保video元素显示控件 if (gv.player.nodeName == "VIDEO") { gv.backControls = gv.player.controls; gv.player.controls = true; } // 触发resize事件(刷新播放器尺寸) window.dispatchEvent(new Event("resize")); }, /** * 退出全屏状态(恢复原始状态) */ smallWin() { // 恢复原始id document.body.parentNode.id = gv.backHtmlId; document.body.id = gv.backBodyId; // 移除父元素的样式类 for (let v of gv.playerParents) { v.classList.remove("parentToothbrush"); v.classList.remove("absoluteToothbrush"); } // 移除播放器的样式类 gv.player.classList.remove("playerToothbrush"); // YouTube特殊处理:恢复剧院模式 if (document.location.hostname == "www.youtube.com" && gv.ytbStageChange) { document.querySelector("#movie_player .ytp-size-button").click(); gv.ytbStageChange = false; } // 恢复video控件状态 if (gv.player.nodeName == "VIDEO") { gv.player.controls = gv.backControls; } // 隐藏辅助按钮 gv.leftBtn.style.display = ""; gv.rightBtn.style.display = ""; gv.controlBtn.style.display = ""; // 恢复鼠标悬停监听 document.addEventListener("mouseover", handle.getPlayer, false); // 触发resize事件 window.dispatchEvent(new Event("resize")); gv.isFull = false; }, }; /** * 初始化脚本 */ const init = () => { // 创建画中画按钮 gv.picinpicBtn = document.createElement("tbdiv"); gv.picinpicBtn.id = "picinpicBtn"; gv.picinpicBtn.onclick = () => { handle.pictureInPicture(); }; // 添加右键菜单事件 gv.picinpicBtn.title = gv.btnText.pipTooltip; // 添加画中画按钮提示 gv.picinpicBtn.addEventListener("contextmenu", handle.showContextMenu); document.body.appendChild(gv.picinpicBtn); // 创建控制按钮和辅助按钮 // gv.controlBtn = tool.createButton("playerControlBtn"); gv.controlBtn = tool.createButton("playerControlBtn", gv.btnText.maxTooltip); gv.leftBtn = tool.createButton("leftFullStackButton"); gv.rightBtn = tool.createButton("rightFullStackButton"); // 添加样式(如果按钮还没有固定定位样式) if (getComputedStyle(gv.controlBtn).position != "fixed") { tool.addStyle( [ // 针对B站播放器的特殊样式 "#htmlToothbrush #bodyToothbrush .parentToothbrush .bilibili-player-video {margin:0 !important;}", // 全屏状态下禁止页面滚动 "#htmlToothbrush, #bodyToothbrush {overflow:hidden !important;zoom:100% !important;}", // 父元素样式重置 "#htmlToothbrush #bodyToothbrush .parentToothbrush {overflow:visible !important;z-index:auto !important;transform:none !important;-webkit-transform-style:flat !important;transition:none !important;contain:none !important;}", // 修正fixed定位的父元素 "#htmlToothbrush #bodyToothbrush .absoluteToothbrush {position:absolute !important;}", // 播放器全屏样式 "#htmlToothbrush #bodyToothbrush .playerToothbrush {position:fixed !important;top:0px !important;left:0px !important;width:100vw !important;height:100vh !important;max-width:none !important;max-height:none !important;min-width:0 !important;min-height:0 !important;margin:0 !important;padding:0 !important;z-index:2147483646 !important;border:none !important;background-color:#000 !important;transform:none !important;}", // 视频内容适配 "#htmlToothbrush #bodyToothbrush .parentToothbrush video {object-fit:contain !important;}", // 视频元素全屏样式 "#htmlToothbrush #bodyToothbrush .parentToothbrush .videoToothbrush {width:100vw !important;height:100vh !important;}", // 全屏按钮样式 '#playerControlBtn {text-shadow: none;visibility:hidden;opacity:0;display:none;transition: all 0.5s ease;cursor: pointer;font: 12px "微软雅黑";margin:0;width:64px;height:20px;line-height:20px;border:none;text-align: center;position: fixed;z-index:2147483647;background-color: #27A9D8;color: #FFF;} #playerControlBtn:hover {visibility:visible;opacity:1;background-color:#2774D8;}', // 画中画按钮样式 '#picinpicBtn {text-shadow: none;visibility:hidden;opacity:0;display:none;transition: all 0.5s ease;cursor: pointer;font: 12px "微软雅黑";margin:0;width:53px;height:20px;line-height:20px;border:none;text-align: center;position: fixed;z-index:2147483647;background-color: #27A9D8;color: #FFF;} #picinpicBtn:hover {visibility:visible;opacity:1;background-color:#2774D8;}', // 左右辅助按钮样式(用于遮挡边缘) "#leftFullStackButton{display:none;position:fixed;width:1px;height:100vh;top:0;left:0;z-index:2147483647;background:#000;}", "#rightFullStackButton{display:none;position:fixed;width:1px;height:100vh;top:0;right:0;z-index:2147483647;background:#000;}", // 右键菜单样式 ` /* 右键菜单默认浅色主题 */ #btnContextMenu { position: fixed; background: rgba(250, 250, 250, 0.72); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); border: 1px solid rgba(255, 255, 255, 0.4); border-radius: 14px; padding: 6px 0; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08); z-index: 2147483647; width: 220px; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 13px; } #btnContextMenu > div:first-child { padding: 8px 20px; color: rgba(60, 60, 67, 0.6); font-weight: 500; font-size: 13px; border-bottom: 1px solid rgba(60, 60, 67, 0.15); cursor: default; } #btnContextMenu > div:not(:first-child) { padding: 5px 10px; cursor: pointer; color: #1c1c1e; border-radius: 8px; transition: background-color 0.2s ease; } #btnContextMenu > div:not(:first-child):hover { background-color: rgba(0, 0, 0, 0.06); } /* 右键菜单深色主题 */ @media (prefers-color-scheme: dark) { #btnContextMenu { background: rgba(28, 28, 30, 0.72); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32); color: rgba(255, 255, 255, 0.85); } #btnContextMenu > div:first-child { color: rgba(235, 235, 245, 0.6); border-bottom: 1px solid rgba(84, 84, 88, 0.65); } #btnContextMenu > div:not(:first-child) { color: rgba(255, 255, 255, 0.85); } #btnContextMenu > div:not(:first-child):hover { /* background-color: rgba(255, 255, 255, 0.10); */ /* 修改为蓝色,为了兼容 Dark Reader */ background-color: rgba(27, 134, 187, 0.2) ; } } `, ].join("\n") ); } // 添加事件监听 document.addEventListener("mouseover", handle.getPlayer, false); document.addEventListener("keydown", handle.hotKey, false); window.addEventListener("message", handle.receiveMessage, false); // 添加鼠标中键点击事件监听 document.addEventListener("mousedown", handle.mouseMiddleClick, false); // 添加全局右键点击事件,用于关闭菜单 document.addEventListener("contextmenu", (e) => { if ( gv.contextMenu && !gv.contextMenu.contains(e.target) && e.target.id !== "playerControlBtn" && e.target.id !== "picinpicBtn" ) { handle.hideContextMenu(); } }); tool.print("Ready"); }; // 初始化脚本 init(); })();