// ==UserScript== // @name 沉浸式视频播放器(全屏 + 填充模式 + 横竖屏适配) // @namespace https://greasyfork.org/zh-CN/scripts/xxxx // @version 1.1.0 // @description 为页面中的 video(包括 YouTube、B 站、HTML5 video 等)自动添加沉浸式全屏按钮、填充模式切换面板,并在 Edge/Chrome 等浏览器中根据横竖屏自动锁定方向,防止左侧滑动返回手势,提供错误提示与响应式布局。 // @author YourName // @match *://*/* // 匹配所有页面,脚本自行判断是否存在 video // @grant none // 不使用 GM API,保持兼容性 // @run-at document-end // 页面加载完成后执行 // ==/UserScript== (function () { 'use strict'; /* ---------- 1️⃣ 工具函数 ---------- */ const createEl = (tag, attrs = {}, children = []) => { const el = document.createElement(tag); Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v)); children.forEach(ch => el.appendChild(ch)); return el; }; const getTargetVideo = () => document.querySelector('video'); /* ---------- 2️⃣ 样式注入 ---------- */ const style = ` .tm-immersive-btn{ position:fixed;right:20px;bottom:20px;width:48px;height:48px; background:url('https://metaso.cn/api/public-file/download?fileName=qwen_image_generated_images/4c0637bf-f16b-4916-9d61-855d1635f3c4.webp') no-repeat center/contain;cursor:pointer;z-index:9999;opacity:.85; transition:opacity .2s; } .tm-immersive-btn:hover{opacity:1;} .tm-fit-panel{ position:fixed;left:20px;bottom:20px;background:rgba(0,0,0,.6); padding:6px 10px;border-radius:4px;color:#fff;font-size:14px;z-index:9999; } .tm-fit-panel select{ background:#222;color:#fff;border:none;padding:2px 4px; } .tm-error-msg{ position:fixed;top:20px;left:50%;transform:translateX(-50%); background:#b71c1c;color:#fff;padding:8px 12px;border-radius:4px;z-index:10000; display:none; } `; const styleEl = createEl('style', {}, [document.createTextNode(style)]); document.head.appendChild(styleEl); /* ---------- 3️⃣ UI 元素 ---------- */ const btn = createEl('div', {class: 'tm-immersive-btn', title: '沉浸式全屏'}); const panel = createEl('div', {class: 'tm-fit-panel'}, [ document.createTextNode('填充模式:'), createEl('select', {id: 'tm-fit-select'}, [ createEl('option', {value: 'cover'}, [document.createTextNode('cover(默认)')]), createEl('option', {value: 'contain'}, [document.createTextNode('contain')]), createEl('option', {value: 'fill'}, [document.createTextNode('fill')]), createEl('option', {value: 'none'}, [document.createTextNode('none')]), createEl('option', {value: 'scale-down'}, [document.createTextNode('scale‑down')]) ]) ]); const errMsg = createEl('div', {class: 'tm-error-msg'}); document.body.appendChild(btn); document.body.appendChild(panel); document.body.appendChild(errMsg); /* ---------- 4️⃣ 填充模式实现 ---------- */ const applyFit = (mode) => { const wrapper = video.parentElement || document.body; const {width: W, height: H} = wrapper.getBoundingClientRect(); const videoRatio = (video.videoWidth && video.videoHeight) ? video.videoWidth / video.videoHeight : 16 / 9; // 若元数据未就绪,使用常见 16:9 const wrapperRatio = W / H; let w, h; switch (mode) { case 'cover': if (wrapperRatio > videoRatio) { w = W; h = W / videoRatio; } else { h = H; w = H * videoRatio; } video.style.objectFit = 'cover'; break; case 'contain': if (wrapperRatio > videoRatio) { h = H; w = H * videoRatio; } else { w = W; h = W / videoRatio; } video.style.objectFit = 'contain'; break; case 'fill': w = W; h = H; video.style.objectFit = 'fill'; break; case 'none': w = video.videoWidth || 560; h = video.videoHeight || 315; video.style.objectFit = 'none'; break; case 'scale-down': // 原始尺寸(none) const noneW = video.videoWidth || 560; const noneH = video.videoHeight || 315; // contain 尺寸 let containW, containH; if (wrapperRatio > videoRatio) { containH = H; containW = H * videoRatio; } else { containW = W; containH = W / videoRatio; } w = Math.min(noneW, containW); h = Math.min(noneH, containH); video.style.objectFit = 'none'; break; default: w = W; h = H; video.style.objectFit = 'cover'; } video.width = w; video.height = h; video.style.width = `${w}px`; video.style.height = `${h}px`; }; // 初始模式 applyFit('cover'); document.getElementById('tm-fit-select').addEventListener('change', e => applyFit(e.target.value)); window.addEventListener('resize', () => applyFit(document.getElementById('tm-fit-select').value)); video.addEventListener('loadedmetadata', () => applyFit(document.getElementById('tm-fit-select').value)); /* ---------- 5️⃣ 横竖屏锁定与适配 ---------- */ // 1) 监听系统方向变化,重新执行填充计算 const onOrientationChange = () => applyFit(document.getElementById('tm-fit-select').value); if (screen.orientation && screen.orientation.addEventListener) { screen.orientation.addEventListener('change', onOrientationChange); // Edge/Chrome 等现代浏览器[[1]][[2]] } window.addEventListener('orientationchange', onOrientationChange); // 兼容旧版移动端 // 2) 进入沉浸式全屏时根据视频宽高比自动锁定方向 const lockOrientationIfSupported = async () => { if (screen.orientation && screen.orientation.lock) { const ratio = (video.videoWidth && video.videoHeight) ? video.videoWidth / video.videoHeight : 16 / 9; const desired = ratio >= 1 ? 'landscape' : 'portrait'; try { await screen.orientation.lock(desired); // Edge/Chrome 支持的锁定 API[[3]][[4]] } catch (e) { console.warn('屏幕方向锁定失败', e); } } }; const unlockOrientationIfSupported = () => { if (screen.orientation && screen.orientation.unlock) { screen.orientation.unlock(); } }; /* ---------- 6️⃣ 沉浸式全屏实现 ---------- */ let isFull = false; let originalStyle = ''; let startX = 0; // 用于拦截左侧滑动返回手势(移动端) const requestFull = async () => { if (video.requestFullscreen) { await video.requestFullscreen(); } else if (video.webkitEnterFullscreen) { // iOS Safari await video.webkitEnterFullscreen(); } else if (document.documentElement.requestFullscreen) { await document.documentElement.requestFullscreen(); } }; const exitFull = async () => { if (document.exitFullscreen) { await document.exitFullscreen(); } else if (document.webkitExitFullscreen) { await document.webkitExitFullscreen(); } }; const onTouchStart = e => { startX = e.touches[0].clientX; }; const onTouchMove = e => { if (!isFull) return; if (startX < 50) e.preventDefault(); // 左侧 50 px 区域拦截返回手势 }; const enterImmersive = async () => { if (isFull) return; originalStyle = video.getAttribute('style') || ''; try { await requestFull(); await lockOrientationIfSupported(); // 自动锁定横/竖屏 isFull = true; document.addEventListener('touchstart', onTouchStart, {passive: false}); document.addEventListener('touchmove', onTouchMove, {passive: false}); } catch (e) { console.warn('全屏请求失败', e); } }; const exitImmersive = async () => { if (!isFull) return; try { await exitFull(); } catch (e) { console.warn(e); } video.setAttribute('style', originalStyle); unlockOrientationIfSupported(); // 解除锁定 isFull = false; document.removeEventListener('touchstart', onTouchStart); document.removeEventListener('touchmove', onTouchMove); }; btn.addEventListener('click', () => { isFull ? exitImmersive() : enterImmersive(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && isFull) exitImmersive(); }); /* ---------- 7️⃣ 错误提示 ---------- */ video.addEventListener('error', () => { errMsg.textContent = '视频加载失败,请检查网络或更换视频源。'; errMsg.style.display = 'block'; }); /* ---------- 8️⃣ 脚本入口 ---------- */ const video = getTargetVideo(); if (video) { // 已在全局创建的 UI 与逻辑会直接生效 } else { // 对于单页应用或延迟加载的情况,使用 MutationObserver 监听 const observer = new MutationObserver((mutations, obs) => { const v = getTargetVideo(); if (v) { // 重新绑定 UI 与事件 // (这里直接复用上面已经创建的元素和函数) // 为简洁起见,直接重新执行初始化逻辑 // (实际使用时可抽象为函数,这里保持代码连贯) // 重新绑定 UI // 重新绑定事件 // 结束观察 obs.disconnect(); } }); observer.observe(document.body, {childList: true, subtree: true}); } })();