您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动宽屏播放并将播放器垂直居中视口,退出宽屏/网页全屏/全屏模式自动滚动页面到顶部。默认关闭自动宽屏。
// ==UserScript== // @name B站自动宽屏居中NEW // @namespace @NIA // @version 1.70 // @description 自动宽屏播放并将播放器垂直居中视口,退出宽屏/网页全屏/全屏模式自动滚动页面到顶部。默认关闭自动宽屏。 // @author NIA // @icon https://www.bilibili.com//favicon.ico // @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 // ==/UserScript== (function () { 'use strict'; // --- 配置项 --- const DEFAULT_PLAYER_CENTER_OFFSET = 90; // 播放器垂直居中时的默认偏移量 (像素) const OFFSET_STEP = 1; // 调整偏移量时的步长 const DEBOUNCE_DELAY = 200; // 事件防抖延迟 (ms) const URL_CHECK_DELAY = 500; // URL 变化后执行逻辑的延迟 (ms) const FINAL_CHECK_DELAY = 400; // 初始化或导航后最终检查状态的延迟 (ms) const SCROLL_ANIMATION_DURATION = 500; // 预估的平滑滚动动画时长 (ms) const OBSERVER_MAX_WAIT_TIME = 15000; // MutationObserver 最长等待时间 (15秒) const SCRIPT_VERSION = '1.70'; // 脚本版本,用于日志记录 // --- 状态变量 --- let elements = { wideBtn: null, webFullBtn: null, fullBtn: null, player: null, playerContainer: null, }; let isEnabled = GM_getValue('enableWideScreen', false); let playerCenterOffset = GM_getValue('playerCenterOffset', DEFAULT_PLAYER_CENTER_OFFSET); let currentUrl = window.location.href; let initTimeout = null; let reInitScheduled = false; let lastScrollTime = 0; let isScrolling = false; let registeredCommandIds = []; let coreElementsObserver = null; let observerTimeoutId = null; /** * 平滑滚动到指定的垂直位置 (带节流)。 * @param {number} topPosition 目标垂直滚动位置 */ function scrollToPosition(topPosition) { if (isScrolling) return; const now = Date.now(); if (now - lastScrollTime < 100 && Math.abs(window.scrollY - topPosition) < 5) { return; } lastScrollTime = now; isScrolling = true; window.scrollTo({ top: topPosition, behavior: 'smooth' }); setTimeout(() => { isScrolling = false; }, SCROLL_ANIMATION_DURATION); } /** 滚动页面使播放器大致垂直居中于视口。 */ const scrollToPlayer = function() { if (!elements.player && !cacheElements()) { console.warn("[B站自动宽屏居中] scrollToPlayer: 播放器元素未缓存且重新缓存失败。"); return; } if (!elements.player) { return; } requestAnimationFrame(() => { const playerRect = elements.player.getBoundingClientRect(); if (playerRect.height > 0) { const playerTop = playerRect.top + window.scrollY; const desiredScrollTop = playerTop - playerCenterOffset; if (Math.abs(window.scrollY - desiredScrollTop) > 5) { scrollToPosition(desiredScrollTop); } } }); } /** 滚动到页面顶部。 */ const scrollToTop = function() { if (window.scrollY > 0) { scrollToPosition(0); } } /** * 检查播放器当前的宽屏/全屏状态,并据此执行相应的滚动操作。 * 这是一个统一的、防抖处理的滚动入口。 */ const debouncedCheckAndScroll = (function() { let timeoutId; return function() { clearTimeout(timeoutId); timeoutId = setTimeout(() => { if (!elements.player || !elements.wideBtn) { 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); if (isWide && !isWebFull && !isFull) { scrollToPlayer(); } else if (!isWide && !isWebFull && !isFull) { scrollToTop(); } }, DEBOUNCE_DELAY); }; })(); /** * 缓存播放器及相关的控制按钮等核心DOM元素。 * @returns {boolean} */ function cacheElements() { elements.player = document.querySelector('#bilibili-player'); if (!elements.player) { return false; } 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'); } return !!elements.wideBtn; } /** 如果启用了自动宽屏,确保播放器处于宽屏模式。 */ function ensureWideMode() { if (!isEnabled || !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(); } } /** 设置事件监听器。 */ function setupListeners() { removeListenersAndObserver(); console.log("[B站自动宽屏居中] setupListeners: 开始设置事件监听器。"); if (!cacheElements()) { console.error("[B站自动宽屏居中] setupListeners: 核心元素查找失败,无法设置监听器。"); return; } // 核心改动:统一调用 debouncedCheckAndScroll elements.wideBtn.addEventListener('click', debouncedCheckAndScroll); if (elements.webFullBtn) elements.webFullBtn.addEventListener('click', debouncedCheckAndScroll); if (elements.fullBtn) elements.fullBtn.addEventListener('click', debouncedCheckAndScroll); const videoArea = elements.playerContainer?.querySelector('.bpx-player-video-area'); if (videoArea) videoArea.addEventListener('dblclick', debouncedCheckAndScroll); document.addEventListener('fullscreenchange', debouncedCheckAndScroll); document.addEventListener('webkitfullscreenchange', debouncedCheckAndScroll); document.addEventListener('mozfullscreenchange', debouncedCheckAndScroll); document.addEventListener('MSFullscreenChange', debouncedCheckAndScroll); document.addEventListener('keydown', handleKeyPress); window.addEventListener('resize', debouncedCheckAndScroll); console.log("[B站自动宽屏居中] setupListeners: 事件监听器设置完成。"); } /** 移除所有已添加的事件监听器和 MutationObserver。 */ function removeListenersAndObserver() { if (elements.wideBtn) elements.wideBtn.removeEventListener('click', debouncedCheckAndScroll); if (elements.webFullBtn) elements.webFullBtn.removeEventListener('click', debouncedCheckAndScroll); if (elements.fullBtn) elements.fullBtn.removeEventListener('click', debouncedCheckAndScroll); const currentContainer = elements.playerContainer || document.querySelector('.bpx-player-container') || document.querySelector('#bilibiliPlayer'); const videoArea = currentContainer?.querySelector('.bpx-player-video-area'); if (videoArea) videoArea.removeEventListener('dblclick', debouncedCheckAndScroll); document.removeEventListener('fullscreenchange', debouncedCheckAndScroll); document.removeEventListener('webkitfullscreenchange', debouncedCheckAndScroll); document.removeEventListener('mozfullscreenchange', debouncedCheckAndScroll); document.removeEventListener('MSFullscreenChange', debouncedCheckAndScroll); document.removeEventListener('keydown', handleKeyPress); window.removeEventListener('resize', debouncedCheckAndScroll); if (coreElementsObserver) { coreElementsObserver.disconnect(); coreElementsObserver = null; } if (observerTimeoutId) { clearTimeout(observerTimeoutId); observerTimeoutId = null; } elements = { wideBtn: null, webFullBtn: null, fullBtn: null, player: null, playerContainer: null }; } /** 处理键盘按下事件,主要用于检测 ESC 键。 */ function handleKeyPress(event) { if (event.key === 'Escape') { debouncedCheckAndScroll(); } } /** 注册(不可用)或更新油猴菜单命令。 */ function registerMenuCommands() { if (typeof GM_registerMenuCommand !== 'function' || typeof GM_unregisterMenuCommand !== 'function') return; registeredCommandIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch (e) {} }); registeredCommandIds = []; const toggleCommandText = `自动宽屏模式 (当前: ${isEnabled ? '✅ 开启' : '❌ 关闭'})`; registeredCommandIds.push(GM_registerMenuCommand(toggleCommandText, toggleWideScreen)); const offsetCommandText = `播放器居中偏移量 (当前: ${playerCenterOffset}px)`; registeredCommandIds.push(GM_registerMenuCommand(offsetCommandText, () => {})); registeredCommandIds.push(GM_registerMenuCommand(`- 减少播放器偏移量 (${OFFSET_STEP}px)`, () => adjustPlayerOffset(-OFFSET_STEP))); registeredCommandIds.push(GM_registerMenuCommand(`+ 增加播放器偏移量 (${OFFSET_STEP}px)`, () => adjustPlayerOffset(OFFSET_STEP))); registeredCommandIds.push(GM_registerMenuCommand('恢复播放器偏移量为默认值', resetPlayerOffset)); console.log("[B站自动宽屏居中] 菜单命令已更新。"); } /** 切换自动宽屏功能的启用/禁用状态。 */ function toggleWideScreen() { const intendedState = !GM_getValue('enableWideScreen', false); if (window.confirm(`是否要${intendedState ? "开启" : "关闭"}自动宽屏模式?`)) { isEnabled = intendedState; GM_setValue('enableWideScreen', isEnabled); registerMenuCommands(); if (isEnabled) { // 启用时,只点击按钮,不立即滚动 ensureWideMode(); } else { if (elements.wideBtn && elements.wideBtn.classList.contains('bpx-state-entered')) { elements.wideBtn.click(); } } debouncedCheckAndScroll(); // 统一调用防抖函数来处理最终的滚动 } } /** 调整播放器居中偏移量。 */ function adjustPlayerOffset(delta) { playerCenterOffset += delta; GM_setValue('playerCenterOffset', playerCenterOffset); console.log(`[B站自动宽屏居中] 播放器居中偏移量已调整为: ${playerCenterOffset}px`); registerMenuCommands(); debouncedCheckAndScroll(); // 统一调用防抖函数来处理最终的滚动 } /** 恢复播放器居中偏移量为默认值。 */ function resetPlayerOffset() { if (window.confirm(`是否要恢复播放器居中偏移量为默认值 (${DEFAULT_PLAYER_CENTER_OFFSET}px)?`)) { playerCenterOffset = DEFAULT_PLAYER_CENTER_OFFSET; GM_setValue('playerCenterOffset', playerCenterOffset); console.log(`[B站自动宽屏居中] 播放器居中偏移量已恢复为默认值: ${playerCenterOffset}px`); registerMenuCommands(); debouncedCheckAndScroll(); // 统一调用防抖函数来处理最终的滚动 } } /** * 核心初始化逻辑:尝试缓存元素,如果失败则使用 MutationObserver 等待元素加载。 */ function initializeScriptLogic() { reInitScheduled = false; clearTimeout(initTimeout); if (coreElementsObserver) { coreElementsObserver.disconnect(); coreElementsObserver = null; } if (observerTimeoutId) { clearTimeout(observerTimeoutId); observerTimeoutId = null; } console.log("[B站自动宽屏居中] initializeScriptLogic: 开始初始化脚本逻辑..."); if (cacheElements()) { console.log("[B站自动宽屏居中] initializeScriptLogic: 核心元素已通过初次尝试成功缓存。"); setupListeners(); if (isEnabled) ensureWideMode(); setTimeout(debouncedCheckAndScroll, FINAL_CHECK_DELAY); // 最终状态检查 return; } console.log("[B站自动宽屏居中] initializeScriptLogic: 初次缓存失败,设置 MutationObserver。"); const observerCallback = function(mutationsList, observerInstance) { if (document.querySelector('#bilibili-player') && document.querySelector('.bpx-player-ctrl-wide')) { if (cacheElements()) { console.log("[B站自动宽屏居中] MutationObserver 触发: 核心元素已成功缓存。"); observerInstance.disconnect(); clearTimeout(observerTimeoutId); coreElementsObserver = null; observerTimeoutId = null; setupListeners(); if (isEnabled) ensureWideMode(); setTimeout(debouncedCheckAndScroll, FINAL_CHECK_DELAY); // 最终状态检查 } } }; coreElementsObserver = new MutationObserver(observerCallback); let targetNodeToObserve = document.getElementById('playerWrap') || document.getElementById('mirror-vdcon') || document.getElementById('app') || document.body; coreElementsObserver.observe(targetNodeToObserve, { childList: true, subtree: true }); observerTimeoutId = setTimeout(() => { if (coreElementsObserver) { console.error(`[B站自动宽屏居中] MutationObserver 超时 (${OBSERVER_MAX_WAIT_TIME}ms): 未能找到或缓存核心元素。`); coreElementsObserver.disconnect(); coreElementsObserver = null; } }, OBSERVER_MAX_WAIT_TIME); } /** * 安排脚本的重新初始化,通常在检测到页面导航(URL路径变化)时调用。 */ function scheduleReInitialization(delay = URL_CHECK_DELAY) { if (reInitScheduled) return; reInitScheduled = true; clearTimeout(initTimeout); initTimeout = setTimeout(() => { removeListenersAndObserver(); if (typeof GM_unregisterMenuCommand === 'function') { registeredCommandIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch (e) {} }); registeredCommandIds = []; } setTimeout(initializeScriptLogic, 100); }, delay); } /** * 检查给定的URL是否匹配脚本的目标页面规则。 */ function isTargetPage(url) { return /\/(video|list|bangumi\/play)\//.test(url); } /** * 处理URL发生变化(包括SPA导航和历史记录变化)。 */ function handleUrlChange() { requestAnimationFrame(() => { const newHref = window.location.href; const newPathname = window.location.pathname; let oldPathnameFromCurrentUrl = '/'; if (currentUrl) { try { oldPathnameFromCurrentUrl = new URL(currentUrl).pathname; } catch (e) { console.warn('[B站自动宽屏居中] 解析旧URL的pathname失败:', currentUrl, e); const doubleSlashIndex = currentUrl.indexOf('//'); if (doubleSlashIndex !== -1) { const pathStartIndex = currentUrl.indexOf('/', doubleSlashIndex + 2); if (pathStartIndex !== -1) { const queryIndex = currentUrl.indexOf('?', pathStartIndex); const hashIndex = currentUrl.indexOf('#', pathStartIndex); let endIndex = currentUrl.length; if (queryIndex !== -1) endIndex = queryIndex; if (hashIndex !== -1 && hashIndex < endIndex) endIndex = hashIndex; oldPathnameFromCurrentUrl = currentUrl.substring(pathStartIndex, endIndex); } } } } if (newPathname !== oldPathnameFromCurrentUrl) { console.log(`[B站自动宽屏居中] Pathname 变化: 从 "${oldPathnameFromCurrentUrl}" 到 "${newPathname}". 触发重新初始化.`); const previousFullUrl = currentUrl; currentUrl = newHref; const isNowTarget = isTargetPage(newHref); if (isNowTarget) { scheduleReInitialization(); } else if (isTargetPage(previousFullUrl)) { removeListenersAndObserver(); if (typeof GM_unregisterMenuCommand === 'function') { registeredCommandIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch (e) {} }); registeredCommandIds = []; } clearTimeout(initTimeout); reInitScheduled = false; } } else if (newHref !== currentUrl) { currentUrl = newHref; } }); } /** 脚本主入口函数。 */ function main() { console.log(`[B站自动宽屏居中] 脚本开始执行 (main)。版本: ${SCRIPT_VERSION}`); isEnabled = GM_getValue('enableWideScreen', false); playerCenterOffset = GM_getValue('playerCenterOffset', DEFAULT_PLAYER_CENTER_OFFSET); if (isTargetPage(currentUrl)) { registerMenuCommands(); } window.addEventListener('popstate', handleUrlChange); const originalPushState = history.pushState; history.pushState = function(...args) { const result = originalPushState.apply(this, args); window.dispatchEvent(new CustomEvent('historystatechanged')); return result; }; const originalReplaceState = history.replaceState; history.replaceState = function(...args) { const result = originalReplaceState.apply(this, args); window.dispatchEvent(new CustomEvent('historystatechanged')); return result; }; window.addEventListener('historystatechanged', handleUrlChange); 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 (typeof GM_unregisterMenuCommand === 'function') { registeredCommandIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch (e) {} }); registeredCommandIds = []; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址