您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将视频播放窗口适配为全页面显示
// ==UserScript== // @name 视频全页面适配·视频倍速播放器·纯净播放 // @namespace https://toolsdar.cn/ // @version 0.4 // @description 将视频播放窗口适配为全页面显示 // @author Your name // @match *://*/* // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; /** ---------------- 存储封装(Tampermonkey 优先,回退 localStorage) ---------------- */ const Storage = (() => { const hasGM = typeof GM_getValue === 'function' && typeof GM_setValue === 'function'; const GET = (k, d) => { try { if (hasGM) return GM_getValue(k, d); const v = localStorage.getItem(k); return v == null ? d : JSON.parse(v); } catch { return d; } }; const SET = (k, v) => { try { if (hasGM) return GM_setValue(k, v); localStorage.setItem(k, JSON.stringify(v)); } catch {} }; return { get: GET, set: SET }; })(); const KEY_ENABLED = 'videoFullpage:enabledSites'; const KEY_POS = 'videoFullpage:btnPos'; // 记录按钮位置 const host = location.hostname; function loadEnabledMap() { const map = Storage.get(KEY_ENABLED, null); return map && typeof map === 'object' ? map : {}; } function saveEnabledMap(map) { Storage.set(KEY_ENABLED, map); } function isSiteEnabled(h = host) { const map = loadEnabledMap(); return !!map[h]; } function setSiteEnabled(enabled, h = host) { const map = loadEnabledMap(); if (enabled) map[h] = true; else delete map[h]; saveEnabledMap(map); } function loadPosMap() { const map = Storage.get(KEY_POS, null); return map && typeof map === 'object' ? map : {}; } function savePosMap(m) { Storage.set(KEY_POS, m); } function getBtnPos(name, h = host) { const m = loadPosMap(); return (m[h] && m[h][name]) ? m[h][name] : null; } function setBtnPos(name, pos, h = host) { const m = loadPosMap(); if (!m[h]) m[h] = {}; m[h][name] = pos; // {left, top} savePosMap(m); } /** ---------------- 工具:等待 body、拖动封装 ---------------- */ function ensureBody(cb) { if (document.body) return cb(); const obs = new MutationObserver(() => { if (document.body) { obs.disconnect(); cb(); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); } /** * 让任意元素可拖动并记住位置 * @param {HTMLElement} el 按钮元素 * @param {string} name 名称:'siteToggle' 或 'actionButton' * @param {object} [opt] { minX, minY, maxX, maxY } 可选 */ function makeDraggable(el, name, opt = {}) { el.style.position = 'fixed'; el.style.cursor = 'move'; // 载入持久化位置 const saved = getBtnPos(name); if (saved && typeof saved.left === 'number' && typeof saved.top === 'number') { el.style.left = saved.left + 'px'; el.style.top = saved.top + 'px'; } else { // 默认位置:左上/右上 if (name === 'siteToggle') { el.style.left = '20px'; el.style.top = '20px'; } else { el.style.right = '20px'; el.style.top = '20px'; } // actionButton 默认在右上 } // 将 right 统一换算为 left,避免混用 if (el.style.right) { const rightPx = parseFloat(getComputedStyle(el).right) || 0; const left = window.innerWidth - el.offsetWidth - rightPx; el.style.right = ''; el.style.left = Math.max(0, left) + 'px'; } let dragging = false; let startX = 0, startY = 0; let origLeft = 0, origTop = 0; let moved = false; // 抑制点击误触 const threshold = 3; const getPoint = (ev) => { if (ev.touches && ev.touches[0]) return { x: ev.touches[0].clientX, y: ev.touches[0].clientY }; return { x: ev.clientX, y: ev.clientY }; }; const clamp = (v, min, max) => Math.min(Math.max(v, min), max); const onDown = (e) => { const p = getPoint(e); dragging = true; moved = false; startX = p.x; startY = p.y; origLeft = parseFloat(getComputedStyle(el).left) || 0; origTop = parseFloat(getComputedStyle(el).top) || 0; document.addEventListener('mousemove', onMove, { passive: false }); document.addEventListener('mouseup', onUp, { passive: false }); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onUp, { passive: false }); e.preventDefault(); e.stopPropagation(); }; const onMove = (e) => { if (!dragging) return; const p = getPoint(e); const dx = p.x - startX; const dy = p.y - startY; if (!moved && Math.hypot(dx, dy) > threshold) moved = true; let newLeft = origLeft + dx; let newTop = origTop + dy; const minX = (typeof opt.minX === 'number') ? opt.minX : 0; const minY = (typeof opt.minY === 'number') ? opt.minY : 0; const maxX = (typeof opt.maxX === 'number') ? opt.maxX : (window.innerWidth - el.offsetWidth); const maxY = (typeof opt.maxY === 'number') ? opt.maxY : (window.innerHeight - el.offsetHeight); newLeft = clamp(newLeft, minX, Math.max(minX, maxX)); newTop = clamp(newTop, minY, Math.max(minY, maxY)); el.style.left = newLeft + 'px'; el.style.top = newTop + 'px'; e.preventDefault(); e.stopPropagation(); }; const onUp = (e) => { if (!dragging) return; dragging = false; // 保存位置 const left = parseFloat(getComputedStyle(el).left) || 0; const top = parseFloat(getComputedStyle(el).top) || 0; setBtnPos(name, { left, top }); document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp); // 如果拖动过,就阻止点击触发 if (moved) { e.preventDefault(); e.stopPropagation(); } }; el.addEventListener('mousedown', onDown); el.addEventListener('touchstart', onDown, { passive: false }); // 点击抑制:若刚拖完,阻止一次 click el.addEventListener('click', (e) => { if (moved) { e.preventDefault(); e.stopPropagation(); moved = false; } }, true); // 窗口尺寸变化时,确保按钮仍在可视范围内 window.addEventListener('resize', () => { const left = parseFloat(getComputedStyle(el).left) || 0; const top = parseFloat(getComputedStyle(el).top) || 0; const clampedLeft = Math.min(left, window.innerWidth - el.offsetWidth); const clampedTop = Math.min(top, window.innerHeight - el.offsetHeight); el.style.left = Math.max(0, clampedLeft) + 'px'; el.style.top = Math.max(0, clampedTop) + 'px'; setBtnPos(name, { left: Math.max(0, clampedLeft), top: Math.max(0, clampedTop) }); }); } /** ---------------- UI:站点启用/禁用开关 ---------------- */ let siteToggleBtn = null; let videoFullpageInstance = null; function renderSiteToggle() { if (siteToggleBtn) return; siteToggleBtn = document.createElement('button'); siteToggleBtn.className = 'vf-site-toggle'; siteToggleBtn.style.cssText = ` position: fixed; left: 20px; top: 20px; z-index: 2147483646; background: rgba(0,0,0,.6); color: #fff; border: none; border-radius: 16px; padding: 6px 10px; font-size: 12px; cursor: move; user-select: none; backdrop-filter: saturate(120%) blur(2px); `; updateSiteToggleText(); siteToggleBtn.addEventListener('click', () => { const enabled = !isSiteEnabled(); setSiteEnabled(enabled); updateSiteToggleText(); // 如果在全页面模式且被关闭,则立即退出 if (!enabled && videoFullpageInstance) { const v = document.querySelector('video.video-fullpage'); if (v) videoFullpageInstance.restoreOriginalState(v); } }); document.body.appendChild(siteToggleBtn); // 让站点按钮可拖动并记住位置 makeDraggable(siteToggleBtn, 'siteToggle'); } function updateSiteToggleText() { const enabled = isSiteEnabled(); if (siteToggleBtn) { siteToggleBtn.textContent = enabled ? '本网站:已启用' : '本网站:未启用'; siteToggleBtn.style.background = enabled ? 'rgba(34,197,94,.85)' : 'rgba(0,0,0,.6)'; } } // 菜单命令(可选):控制当前站点开关 if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('切换本网站开关', () => { const enabled = !isSiteEnabled(); setSiteEnabled(enabled); updateSiteToggleText(); alert(`已${enabled ? '启用' : '禁用'}:${host}`); }); GM_registerMenuCommand('清空所有站点开关', () => { Storage.set(KEY_ENABLED, {}); alert('已清空所有站点的启用状态'); updateSiteToggleText(); }); GM_registerMenuCommand('清空按钮位置', () => { const map = loadPosMap(); if (map[host]) delete map[host]; savePosMap(map); alert('已清空本域名下的按钮位置,刷新后恢复默认'); }); } /** ---------------- 原有功能(仅在启用站点时可用) ---------------- */ class VideoFullpage { constructor() { this.handleGlobalKey = this.handleGlobalKey.bind(this); document.addEventListener('keydown', this.handleGlobalKey, { capture: true }); this.originalStates = new Map(); } // 仅站点启用时响应热键 handleGlobalKey(e) { if (!isSiteEnabled()) return; if (e.code === 'KeyH' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); const video = document.querySelector('video'); if (!video) return; if (video.classList.contains('video-fullpage')) { this.toggleFullpage(video); } else { if (!this.initialized) { this.createButton(); this.initialized = true; } this.toggleFullpage(video); } } } createButton() { const button = document.createElement('button'); this.button = button; button.innerHTML = '全页面'; button.style.cssText = ` position: fixed; right: 20px; top: 20px; z-index: 2147483646; background: rgba(0, 0, 0, 0.6); color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: move; font-size: 14px; transition: background-color 0.3s; display: none; `; const style = document.createElement('style'); style.textContent = `.active { background: rgba(0, 0, 0, 0.9) !important; }`; document.head.appendChild(style); document.body.appendChild(button); // 点击(非拖动)触发 button.addEventListener('click', () => { if (!isSiteEnabled()) return; const video = document.querySelector('video'); if (video) { this.toggleFullpage(video); button.classList.toggle('active'); } }); // 可拖动并记住位置(名称为 actionButton) makeDraggable(button, 'actionButton'); } toggleFullpage(video) { if (video.classList.contains('video-fullpage')) { this.restoreOriginalState(video); if (this.button) { this.button.classList.remove('active'); this.button.style.display = 'none'; } } else { this.saveOriginalState(video); this.enterFullpage(video); if (this.button) { this.button.classList.add('active'); this.button.style.display = 'block'; } } } saveOriginalState(video) { const originalState = { style: video.style.cssText, parentNode: video.parentNode, nextSibling: video.nextSibling, scrollTop: window.scrollY, scrollLeft: window.scrollX, bodyOverflow: document.body.style.overflow, bodyPosition: document.body.style.position, videoPosition: { width: video.offsetWidth, height: video.offsetHeight, rect: video.getBoundingClientRect() } }; this.originalStates.set(video, originalState); } restoreOriginalState(video) { const state = this.originalStates.get(video); if (!state) return; if (this.videoEvents) { video.removeEventListener('click', this.videoEvents.click); document.removeEventListener('keydown', this.videoEvents.keydown); this.videoEvents = null; } const container = document.querySelector('.video-fullpage-container'); if (container) { if (state.parentNode) { if (state.nextSibling) state.parentNode.insertBefore(video, state.nextSibling); else state.parentNode.appendChild(video); } container.remove(); } video.classList.remove('video-fullpage'); video.style.cssText = state.style || ''; document.body.style.overflow = state.bodyOverflow || ''; document.body.style.position = state.bodyPosition || ''; requestAnimationFrame(() => { window.scrollTo({ left: state.scrollLeft || 0, top: state.scrollTop || 0, behavior: 'instant' }); }); window.removeEventListener('resize', this.resizeHandler); this.originalStates.delete(video); const hint = document.querySelector('.video-seek-hint'); if (hint) hint.remove(); video.style.cssText = state.style || ''; video.style.width = ''; video.style.height = ''; video.style.maxWidth = '100%'; video.style.maxHeight = '100%'; video.dispatchEvent(new CustomEvent('exitfullpage')); // 触发布局回流 // eslint-disable-next-line no-unused-expressions video.offsetHeight; if (this.progressCleanup) { this.progressCleanup(); this.progressCleanup = null; } } enterFullpage(video) { const container = document.createElement('div'); container.className = 'video-fullpage-container'; container.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.9); z-index: 2147483646; display: flex; justify-content: center; align-items: center; `; video.classList.add('video-fullpage'); const updateVideoSize = () => { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const videoRatio = video.videoWidth / video.videoHeight; const windowRatio = windowWidth / windowHeight; if (windowRatio > videoRatio) { video.style.cssText = ` height: ${windowHeight}px !important; width: ${windowHeight * videoRatio}px !important; position: relative !important; z-index: 2147483647 !important; background: transparent !important; margin: 0 !important; padding: 0 !important; cursor: pointer !important; `; } else { video.style.cssText = ` width: ${windowWidth}px !important; height: ${windowWidth / videoRatio}px !important; position: relative !important; z-index: 2147483647 !important; background: transparent !important; margin: 0 !important; padding: 0 !important; cursor: pointer !important; `; } }; this.resizeHandler = updateVideoSize; window.addEventListener('resize', this.resizeHandler); const handleClick = (e) => { e.preventDefault(); e.stopPropagation(); if (video.paused) video.play(); else video.pause(); }; const handleKeydown = (e) => { if (!video.classList.contains('video-fullpage')) return; switch (e.code) { case 'Space': e.preventDefault(); if (video.paused) video.play(); else video.pause(); break; case 'ArrowLeft': case 'KeyA': e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 3); this.showSeekHint(video, '⏪ -3s'); break; case 'ArrowRight': case 'KeyD': e.preventDefault(); video.currentTime = Math.min(video.duration, video.currentTime + 3); this.showSeekHint(video, '⏩ +3s'); break; case 'ArrowUp': case 'KeyW': e.preventDefault(); video.volume = Math.min(1, video.volume + 0.1); this.showSeekHint(video, `🔊 ${Math.round(video.volume * 100)}%`); break; case 'ArrowDown': case 'KeyS': e.preventDefault(); video.volume = Math.max(0, video.volume - 0.1); this.showSeekHint(video, `🔉 ${Math.round(video.volume * 100)}%`); break; case 'KeyC': e.preventDefault(); video.playbackRate = Math.min(16, video.playbackRate + 0.1); this.showSeekHint(video, `⏩ ${video.playbackRate.toFixed(1)}x`); break; case 'KeyX': e.preventDefault(); video.playbackRate = Math.max(0.1, video.playbackRate - 0.1); this.showSeekHint(video, `⏪ ${video.playbackRate.toFixed(1)}x`); break; case 'KeyZ': e.preventDefault(); video.playbackRate = 1.0; this.showSeekHint(video, '⏮ 1.0x'); break; } }; video.addEventListener('click', handleClick); document.addEventListener('keydown', handleKeydown); this.videoEvents = { click: handleClick, keydown: handleKeydown }; container.appendChild(video); document.body.appendChild(container); document.body.style.overflow = 'hidden'; if (video.readyState >= 1) updateVideoSize(); else video.addEventListener('loadedmetadata', updateVideoSize, { once: true }); // 进度条 const progressContainer = document.createElement('div'); progressContainer.className = 'video-progress-container'; progressContainer.style.cssText = ` position: absolute; bottom: 0; left: 0; right: 0; height: 40px; background: linear-gradient(transparent, rgba(0,0,0,.7)); opacity: 0; transition: opacity .3s; display: flex; align-items: center; padding: 0 20px; z-index: 2147483647; `; const progress = document.createElement('div'); progress.className = 'video-progress'; progress.style.cssText = ` position: relative; width: 100%; height: 4px; background: rgba(255,255,255,.3); border-radius: 2px; cursor: pointer; `; const progressFill = document.createElement('div'); progressFill.className = 'video-progress-fill'; progressFill.style.cssText = ` position: absolute; left: 0; top: 0; height: 100%; background: #ff0000; border-radius: 2px; `; const timeDisplay = document.createElement('div'); timeDisplay.className = 'video-time-display'; timeDisplay.style.cssText = ` color: #fff; margin-left: 10px; font-size: 14px; min-width: 100px; text-align: right; `; progress.appendChild(progressFill); progressContainer.appendChild(progress); progressContainer.appendChild(timeDisplay); const formatTime = (seconds) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; return `${m}:${s.toString().padStart(2, '0')}`; }; const updateProgress = () => { const percent = (video.currentTime / video.duration) * 100; progressFill.style.width = `${percent}%`; timeDisplay.textContent = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`; }; let isDragging = false; const handleProgressClick = (e) => { const rect = progress.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; video.currentTime = video.duration * Math.min(1, Math.max(0, percent)); updateProgress(); }; progress.addEventListener('mousedown', (e) => { isDragging = true; handleProgressClick(e); }); document.addEventListener('mousemove', (e) => { if (isDragging) handleProgressClick(e); }); document.addEventListener('mouseup', () => { isDragging = false; }); let hideTimeout; container.addEventListener('mousemove', () => { progressContainer.style.opacity = '1'; clearTimeout(hideTimeout); hideTimeout = setTimeout(() => { if (!isDragging) progressContainer.style.opacity = '0'; }, 2000); }); container.addEventListener('mouseleave', () => { if (!isDragging) progressContainer.style.opacity = '0'; }); video.addEventListener('timeupdate', updateProgress); container.appendChild(progressContainer); const cleanup = () => { video.removeEventListener('timeupdate', updateProgress); clearTimeout(hideTimeout); }; this.progressCleanup = cleanup; } showSeekHint(video, text) { const existingHint = document.querySelector('.video-seek-hint'); if (existingHint) existingHint.remove(); const hint = document.createElement('div'); hint.className = 'video-seek-hint'; hint.textContent = text; hint.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,.8); color: #fff; padding: 10px 20px; border-radius: 4px; font-size: 16px; pointer-events: none; z-index: 2147483648; animation: vf-fadeOut .5s ease-out .5s forwards; `; const style = document.createElement('style'); style.textContent = `@keyframes vf-fadeOut { from { opacity: 1; } to { opacity: 0; } }`; document.head.appendChild(style); const container = document.querySelector('.video-fullpage-container'); if (container) { container.appendChild(hint); setTimeout(() => hint.remove(), 1000); } } } // 初始化 UI 与实例 ensureBody(() => { renderSiteToggle(); videoFullpageInstance = new VideoFullpage(); }); })();