// ==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();
})();