您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
自动宽屏播放并将播放器垂直居中视口,退出宽屏/网页全屏/全屏模式自动滚动页面到顶部。默认关闭自动宽屏。
当前为
// ==UserScript== // @name B站自动宽屏居中 // @namespace @ChatGPT // @version 1.64 // @description 自动宽屏播放并将播放器垂直居中视口,退出宽屏/网页全屏/全屏模式自动滚动页面到顶部。默认关闭自动宽屏。 // @author Gemini;wha4up // @license MIT // @match https://*.bilibili.com/video/* // @match https://*.bilibili.com/list/* // @match https://*.bilibili.com/bangumi/play/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-idle // @supportURL http://greasyfork.icu/zh-CN/scripts/492413-b%E7%AB%99%E8%87%AA%E5%8A%A8%E5%AE%BD%E5%B1%8F%E5%B1%85%E4%B8%AD/feedback // @homepageURL http://greasyfork.icu/zh-CN/scripts/492413-b%E7%AB%99%E8%87%AA%E5%8A%A8%E5%AE%BD%E5%B1%8F%E5%B1%85%E4%B8%AD // ==/UserScript== (function () { 'use strict'; // --- 配置项 --- const PLAYER_CENTER_OFFSET = 75; // 播放器垂直居中时的偏移量 (像素) const CHECK_INTERVAL = 500; // 查找元素的间隔时间 (ms) const MAX_ATTEMPTS = 20; // 查找元素的最大尝试次数 const DEBOUNCE_DELAY = 200; // 事件防抖延迟 (ms) const URL_CHECK_DELAY = 500; // URL 变化后执行逻辑的延迟 (ms) const FINAL_CHECK_DELAY = 300; // 初始化或导航后最终检查状态的延迟 (ms) const SCROLL_ANIMATION_DURATION = 500; // 预估的平滑滚动动画时长 (ms),用于滚动节流 // --- 状态变量 --- let elements = { wideBtn: null, webFullBtn: null, fullBtn: null, player: null, playerContainer: null, }; let isEnabled = GM_getValue('enableWideScreen', false); let currentUrl = window.location.href; let initTimeout = null; let reInitScheduled = false; let observer = null; let lastScrollTime = 0; let isScrolling = false; let currentMenuCommandText = ''; // 用于存储当前注册的菜单命令文本 // --- 工具函数 --- /** 防抖函数 */ function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } /** 等待指定元素出现在 DOM 中 */ function waitForElement(selector, callback, interval = CHECK_INTERVAL, maxAttempts = MAX_ATTEMPTS) { let attempts = 0; let intervalId = setInterval(() => { const element = document.querySelector(selector); if (element) { clearInterval(intervalId); callback(element); } else { attempts++; if (attempts >= maxAttempts) { clearInterval(intervalId); console.warn(`[B站自动宽屏居中] 元素 "${selector}" 未在 ${maxAttempts * interval}ms 内找到。`); } } }, interval); } /** 平滑滚动到指定垂直位置 (带节流) */ function scrollToPosition(topPosition) { if (isScrolling) return; const now = Date.now(); if (now - lastScrollTime < 100 && Math.abs(window.scrollY - topPosition) < 50) { return; } lastScrollTime = now; isScrolling = true; window.scrollTo({ top: topPosition, behavior: 'smooth' }); setTimeout(() => { isScrolling = false; }, SCROLL_ANIMATION_DURATION); } /** 滚动页面使播放器垂直居中 */ function scrollToPlayer() { if (!elements.player && !cacheElements()) return; if (!elements.player) return; requestAnimationFrame(() => { const playerRect = elements.player.getBoundingClientRect(); if (playerRect.height > 0) { const playerTop = playerRect.top + window.scrollY; const desiredScrollTop = playerTop - PLAYER_CENTER_OFFSET; if (Math.abs(window.scrollY - desiredScrollTop) > 5) { scrollToPosition(desiredScrollTop); } } }); } /** 滚动到页面顶部 */ function scrollToTop() { if (window.scrollY > 0) { scrollToPosition(0); } } /** 缓存播放器及控制按钮等元素 */ function cacheElements() { elements.player = document.querySelector('#bilibili-player'); elements.playerContainer = document.querySelector('.bpx-player-container') || document.querySelector('#bilibiliPlayer') || elements.player; if (elements.playerContainer) { elements.wideBtn = elements.playerContainer.querySelector('.bpx-player-ctrl-wide'); elements.webFullBtn = elements.playerContainer.querySelector('.bpx-player-ctrl-web'); elements.fullBtn = elements.playerContainer.querySelector('.bpx-player-ctrl-full'); } else { elements.wideBtn = document.querySelector('.bpx-player-ctrl-wide'); elements.webFullBtn = document.querySelector('.bpx-player-ctrl-web'); elements.fullBtn = document.querySelector('.bpx-player-ctrl-full'); } if (!elements.wideBtn) { console.warn("[B站自动宽屏居中] 未找到宽屏按钮 '.bpx-player-ctrl-wide'"); return false; } return !!elements.player; } /** 检查播放器状态并执行相应滚动操作 (核心逻辑) */ function checkAndScroll() { if (!cacheElements()) return; const isWide = elements.wideBtn.classList.contains('bpx-state-entered'); const isWebFull = elements.webFullBtn && elements.webFullBtn.classList.contains('bpx-state-entered'); const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); // 只有当没有浮窗播放器时,才执行滚动逻辑 if (isWide && !isWebFull && !isFull) { scrollToPlayer(); } else if (!isWide && !isWebFull && !isFull) { scrollToTop(); } } /** 防抖版的 checkAndScroll,用于 resize 事件 */ const debouncedCheckAndScroll = debounce(checkAndScroll, DEBOUNCE_DELAY); /** 启用或确保宽屏模式(仅在自动宽屏启用时由脚本主动调用) */ function ensureWideMode() { if (!isEnabled || !elements.wideBtn && !cacheElements() || !elements.wideBtn) return; const isCurrentlyWide = elements.wideBtn.classList.contains('bpx-state-entered'); const isWebFull = elements.webFullBtn && elements.webFullBtn.classList.contains('bpx-state-entered'); const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement); if (!isCurrentlyWide && !isWebFull && !isFull) { elements.wideBtn.click(); } else if (isCurrentlyWide && !isWebFull && !isFull) { checkAndScroll(); } } /** 设置事件监听器和 MutationObserver */ function setupListenersAndObserver() { removeListenersAndObserver(); if (!cacheElements() || !elements.wideBtn) { console.error("[B站自动宽屏居中] setupListeners: 核心元素查找失败。"); return; } elements.wideBtn.addEventListener('click', handleWideBtnClick); // 使用新的处理函数 if (elements.webFullBtn) elements.webFullBtn.addEventListener('click', checkAndScroll); if (elements.fullBtn) elements.fullBtn.addEventListener('click', checkAndScroll); const videoArea = elements.playerContainer?.querySelector('.bpx-player-video-area'); if (videoArea) videoArea.addEventListener('dblclick', checkAndScroll); document.addEventListener('fullscreenchange', checkAndScroll); document.addEventListener('webkitfullscreenchange', checkAndScroll); document.addEventListener('mozfullscreenchange', checkAndScroll); document.addEventListener('MSFullscreenChange', checkAndScroll); document.addEventListener('keydown', handleKeyPress); window.addEventListener('resize', debouncedCheckAndScroll); } /** 移除所有添加的事件监听器和 MutationObserver */ function removeListenersAndObserver() { if (elements.wideBtn) elements.wideBtn.removeEventListener('click', handleWideBtnClick); // 移除新的处理函数 if (elements.webFullBtn) elements.webFullBtn.removeEventListener('click', checkAndScroll); if (elements.fullBtn) elements.fullBtn.removeEventListener('click', checkAndScroll); const videoArea = elements.playerContainer?.querySelector('.bpx-player-video-area'); if (videoArea) videoArea.removeEventListener('dblclick', checkAndScroll); document.removeEventListener('fullscreenchange', checkAndScroll); document.removeEventListener('webkitfullscreenchange', checkAndScroll); document.removeEventListener('mozfullscreenchange', checkAndScroll); document.removeEventListener('MSFullscreenChange', checkAndScroll); document.removeEventListener('keydown', handleKeyPress); window.removeEventListener('resize', debouncedCheckAndScroll); if (observer) { observer.disconnect(); observer = null; } elements = { wideBtn: null, webFullBtn: null, fullBtn: null, player: null, playerContainer: null }; } /** 处理键盘按下事件 (主要处理 ESC 键) */ function handleKeyPress(event) { if (event.key === 'Escape') { setTimeout(checkAndScroll, 150); } } /** 注册油猴菜单命令 */ function registerMenuCommand() { // 检查 GM API 是否可用 if (typeof GM_registerMenuCommand !== 'function' || typeof GM_unregisterMenuCommand !== 'function') return; // 构造新命令的文本 const newCommandText = `自动宽屏模式 (当前: ${isEnabled ? '✅ 开启' : '❌ 关闭'})`; // 1. 尝试移除上一次注册的命令 (如果文本不同) // 这可以避免在状态未实际改变时进行不必要的注销和注册 if (currentMenuCommandText && currentMenuCommandText !== newCommandText) { try { GM_unregisterMenuCommand(currentMenuCommandText); } catch (e) { console.warn("[B站自动宽屏居中] 注销旧菜单命令失败:", e); } } // 2. 注册新命令 try { GM_registerMenuCommand(newCommandText, toggleWideScreen); // 3. 更新当前命令文本记录 currentMenuCommandText = newCommandText; } catch (e) { console.error("[B站自动宽屏居中] 注册新菜单命令失败:", e); } } /** 切换自动宽屏功能的启用状态 (带确认框) */ function toggleWideScreen() { const currentState = GM_getValue('enableWideScreen', false); const intendedState = !currentState; const actionText = intendedState ? "开启" : "关闭"; const confirmationMessage = `是否要${actionText}自动宽屏模式?`; if (window.confirm(confirmationMessage)) { // 用户确认 isEnabled = intendedState; GM_setValue('enableWideScreen', isEnabled); registerMenuCommand(); // 更新菜单显示 // 应用效果 if (isEnabled) { ensureWideMode(); } else { // 关闭自动宽屏时,如果为宽屏模式则模拟点击宽屏按钮 if (elements.wideBtn && elements.wideBtn.classList.contains('bpx-state-entered')) { elements.wideBtn.click(); } checkAndScroll(); } } // 用户取消则不执行任何操作 } /** 处理宽屏按钮点击事件 */ function handleWideBtnClick() { checkAndScroll(); // 先执行一次检查和滚动 // 延迟一段时间后再次检查和滚动,以确保状态更新 setTimeout(checkAndScroll, 200); } /** 核心初始化逻辑 */ function initializeScriptLogic() { reInitScheduled = false; clearTimeout(initTimeout); waitForElement('#bilibili-player, .bpx-player-container', () => { if (cacheElements()) { setupListenersAndObserver(); if (isEnabled) { ensureWideMode(); } setTimeout(checkAndScroll, FINAL_CHECK_DELAY); // 最终状态检查 } else { console.error("[B站自动宽屏居中] 初始化失败:未能缓存核心元素。"); } }); } /** 安排重新初始化脚本逻辑 (用于 SPA 导航) */ function scheduleReInitialization(delay = URL_CHECK_DELAY) { if (reInitScheduled) return; reInitScheduled = true; clearTimeout(initTimeout); initTimeout = setTimeout(() => { removeListenersAndObserver(); // 清理旧的 setTimeout(initializeScriptLogic, 100); // 延迟后初始化 }, delay); } /** 检查 URL 是否是目标页面 */ function isTargetPage(url) { return /\/(video|list|bangumi\/play)\//.test(url); } /** 处理 URL 发生变化 */ function handleUrlChange() { requestAnimationFrame(() => { const newUrl = window.location.href; if (newUrl !== currentUrl) { const oldUrl = currentUrl; currentUrl = newUrl; const wasTarget = isTargetPage(oldUrl); const isNowTarget = isTargetPage(newUrl); if (isNowTarget) { scheduleReInitialization(); } else if (wasTarget && !isNowTarget) { removeListenersAndObserver(); clearTimeout(initTimeout); reInitScheduled = false; } } }); } /** 主函数:脚本入口 */ function main() { isEnabled = GM_getValue('enableWideScreen', false); registerMenuCommand(); // 初始注册菜单 // --- 监听 URL 变化 --- window.addEventListener('popstate', handleUrlChange); const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { const result = originalPushState.apply(this, args); window.dispatchEvent(new CustomEvent('historystatechanged')); return result; }; history.replaceState = function(...args) { const result = originalReplaceState.apply(this, args); window.dispatchEvent(new CustomEvent('historystatechanged')); return result; }; window.addEventListener('historystatechanged', handleUrlChange); // --- 初始加载处理 --- currentUrl = window.location.href; if (isTargetPage(currentUrl)) { initializeScriptLogic(); } // --- 清理工作 (页面卸载时) --- window.addEventListener('unload', () => { removeListenersAndObserver(); history.pushState = originalPushState; history.replaceState = originalReplaceState; window.removeEventListener('historystatechanged', handleUrlChange); window.removeEventListener('popstate', handleUrlChange); clearTimeout(initTimeout); }); } // --- 启动脚本 --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();