您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动宽屏播放并将播放器垂直居中视口,退出宽屏/网页全屏/全屏模式自动滚动页面到顶部。默认关闭自动宽屏。
// ==UserScript== // @name B站自动宽屏居中 // @namespace @ChatGPT // @version 1.65 // @description 自动宽屏播放并将播放器垂直居中视口,退出宽屏/网页全屏/全屏模式自动滚动页面到顶部。默认关闭自动宽屏。 // @author Gemini, wha4up (AI Optimized) // @icon data:image/webp;base64,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 // @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 https://greasyfork.org/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 https://greasyfork.org/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; // (waitForElement) 查找元素的间隔时间 (ms) const MAX_ATTEMPTS = 20; // (waitForElement) 查找元素的最大尝试次数 const DEBOUNCE_DELAY = 200; // 事件防抖延迟 (ms) const URL_CHECK_DELAY = 500; // URL 变化后执行逻辑的延迟 (ms) const FINAL_CHECK_DELAY = 300; // 初始化或导航后最终检查状态的延迟 (ms) const SCROLL_ANIMATION_DURATION = 500;// 预估的平滑滚动动画时长 (ms) const OBSERVER_MAX_WAIT_TIME = 15000; // MutationObserver 最长等待时间 (15秒) const SCRIPT_VERSION = '1.68-final-review'; // 脚本版本,用于日志记录 // --- 状态变量 --- let elements = { wideBtn: null, webFullBtn: null, fullBtn: null, player: null, playerContainer: null, }; let isEnabled = GM_getValue('enableWideScreen', false); // 自动宽屏功能是否启用 let currentUrl = window.location.href; // 当前页面的完整URL let initTimeout = null; // 初始化延迟计时器 let reInitScheduled = false; // 是否已计划重新初始化 let lastScrollTime = 0; // 上次滚动时间,用于滚动节流 let isScrolling = false; // 是否正在执行平滑滚动 let currentMenuCommandText = ''; // 当前注册的油猴菜单命令文本 let coreElementsObserver = null; // 用于核心元素加载的 MutationObserver let observerTimeoutId = null; // MutationObserver 的安全超时计时器 // --- 工具函数 --- /** * 防抖函数:在事件触发后等待指定延迟,若期间无新触发则执行函数。 * @param {Function} func 需要防抖的函数 * @param {number} delay 延迟时间 (ms) * @returns {Function} 防抖处理后的函数 */ function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } /** * (辅助函数) 通过轮询等待指定元素出现在 DOM 中。 * 主要用于 MutationObserver 机制之外的特定元素等待场景。 * @param {string} selector CSS选择器 * @param {Function} callback 元素找到后的回调函数,参数为找到的元素或null * @param {number} interval 检查间隔 (ms) * @param {number} maxAttempts 最大尝试次数 */ function waitForElement(selector, callback, interval = CHECK_INTERVAL, maxAttempts = MAX_ATTEMPTS) { let attempts = 0; // console.log(`[B站自动宽屏居中] (waitForElement) 开始等待元素 "${selector}"`); // 调试时可取消注释 let intervalId = setInterval(() => { const element = document.querySelector(selector); if (element) { // console.log(`[B站自动宽屏居中] (waitForElement) 元素 "${selector}" 已找到`); // 调试时可取消注释 clearInterval(intervalId); callback(element); } else { attempts++; if (attempts >= maxAttempts) { clearInterval(intervalId); console.warn(`[B站自动宽屏居中] (waitForElement) 元素 "${selector}" 未在 ${maxAttempts * interval}ms 内找到。`); if (typeof callback === 'function') callback(null); } } }, interval); } /** * 平滑滚动到指定的垂直位置 (带节流)。 * @param {number} topPosition 目标垂直滚动位置 */ 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()) { console.warn("[B站自动宽屏居中] scrollToPlayer: 播放器元素未缓存且重新缓存失败。"); return; } // 再次检查,确保 elements.player 有效 if (!elements.player) { console.error("[B站自动宽屏居中] scrollToPlayer: 播放器元素 (elements.player) 仍然无效。"); return; } // 使用 requestAnimationFrame 优化滚动性能 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); } } /** * 缓存播放器及相关的控制按钮等核心DOM元素。 * @returns {boolean} 如果核心元素(播放器和宽屏按钮)成功缓存则返回true,否则返回false。 */ function cacheElements() { // console.log("[B站自动宽屏居中] 开始缓存元素 (cacheElements)..."); // 调试时可取消注释 elements.player = document.querySelector('#bilibili-player'); if (!elements.player) { console.warn("[B站自动宽屏居中] cacheElements: 核心播放器元素 '#bilibili-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 { // 如果播放器容器未明确找到(理论上不太可能,因为有 fallback),尝试全局查找按钮 console.warn("[B站自动宽屏居中] cacheElements: 'elements.playerContainer' 未明确找到,尝试全局查找按钮。"); 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站自动宽屏居中] cacheElements: 未找到宽屏按钮 '.bpx-player-ctrl-wide'。"); return false; // 宽屏按钮对于核心功能也是必需的 } // 网页全屏和全屏按钮不是核心功能所必需的,仅记录警告 if (!elements.webFullBtn) { // console.warn("[B站自动宽屏居中] cacheElements: 未找到网页全屏按钮 '.bpx-player-ctrl-web'."); } if (!elements.fullBtn) { // console.warn("[B站自动宽屏居中] cacheElements: 未找到全屏按钮 '.bpx-player-ctrl-full'."); } return true; // 播放器和宽屏按钮都找到,视为成功 } /** 检查播放器当前的宽屏/全屏状态,并据此执行相应的滚动操作。 */ function checkAndScroll() { // 确保核心元素已缓存 if (!elements.player || !elements.wideBtn) { if (!cacheElements()) { // 如果未缓存,尝试再次缓存 console.error("[B站自动宽屏居中] checkAndScroll: 核心元素缓存失败,无法执行滚动逻辑。"); 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) return; // 如果未启用自动宽屏,则不执行任何操作 // 确保宽屏按钮已缓存 if (!elements.wideBtn && !cacheElements()) { console.error("[B站自动宽屏居中] ensureWideMode: 宽屏按钮无法缓存。"); return; } if (!elements.wideBtn) { // 再次检查 console.error("[B站自动宽屏居中] ensureWideMode: 宽屏按钮 (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(); setTimeout(checkAndScroll, 200); // 点击后稍作延迟再检查滚动状态 } else if (isCurrentlyWide && !isWebFull && !isFull) { // 如果已经是宽屏模式(且非其他全屏),则执行一次检查滚动 checkAndScroll(); } } /** 设置事件监听器。 */ function setupListeners() { removeListenersAndObserver(); // 先移除旧的监听器和Observer,确保清洁状态 console.log("[B站自动宽屏居中] setupListeners: 开始设置事件监听器。"); if (!cacheElements()) { // 确保元素已缓存 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); // 兼容 WebKit 内核 document.addEventListener('mozfullscreenchange', checkAndScroll); // 兼容 Firefox document.addEventListener('MSFullscreenChange', checkAndScroll); // 兼容 IE/Edge (旧版) // 监听键盘事件 (主要用于处理 ESC 键退出全屏) document.addEventListener('keydown', handleKeyPress); // 监听窗口大小变化事件 window.addEventListener('resize', debouncedCheckAndScroll); console.log("[B站自动宽屏居中] setupListeners: 事件监听器设置完成。"); } /** 移除所有已添加的事件监听器和 MutationObserver。 */ function removeListenersAndObserver() { console.log("[B站自动宽屏居中] removeListenersAndObserver: 开始清理监听器和Observer。"); // 移除按钮点击事件 if (elements.wideBtn) elements.wideBtn.removeEventListener('click', handleWideBtnClick); if (elements.webFullBtn) elements.webFullBtn.removeEventListener('click', checkAndScroll); if (elements.fullBtn) elements.fullBtn.removeEventListener('click', checkAndScroll); // 移除视频区域双击事件 (需要重新获取容器以防 elements 对象被清空) 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', 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); // 断开并清理 MutationObserver if (coreElementsObserver) { coreElementsObserver.disconnect(); coreElementsObserver = null; console.log("[B站自动宽屏居中] CoreElements MutationObserver 已断开。"); } if (observerTimeoutId) { clearTimeout(observerTimeoutId); observerTimeoutId = null; } // 重置缓存的元素对象 elements = { wideBtn: null, webFullBtn: null, fullBtn: null, player: null, playerContainer: null }; console.log("[B站自动宽屏居中] removeListenersAndObserver: 清理完成。"); } /** 处理键盘按下事件,主要用于检测 ESC 键。 */ function handleKeyPress(event) { if (event.key === 'Escape') { // ESC 键通常用于退出全屏或网页全屏,延迟检查状态 setTimeout(checkAndScroll, 150); } } /** 注册或更新油猴菜单命令,用于切换自动宽屏功能的启用状态。 */ function registerMenuCommand() { // 确保 GM API 可用 if (typeof GM_registerMenuCommand !== 'function' || typeof GM_unregisterMenuCommand !== 'function') return; const newCommandText = `自动宽屏模式 (当前: ${isEnabled ? '✅ 开启' : '❌ 关闭'})`; // 如果菜单文本发生变化,先尝试注销旧命令 if (currentMenuCommandText && currentMenuCommandText !== newCommandText) { try { GM_unregisterMenuCommand(currentMenuCommandText); } catch (e) { console.warn("[B站自动宽屏居中] 注销旧菜单命令失败:", e); } } // 注册新命令 try { GM_registerMenuCommand(newCommandText, toggleWideScreen); currentMenuCommandText = newCommandText; // 更新当前命令文本记录 } catch (e) { console.error("[B站自动宽屏居中] 注册新菜单命令失败:", e); } } /** 切换自动宽屏功能的启用/禁用状态。 */ function toggleWideScreen() { const intendedState = !GM_getValue('enableWideScreen', false); // 获取预期的下一个状态 // 弹出确认框,让用户确认操作 if (window.confirm(`是否要${intendedState ? "开启" : "关闭"}自动宽屏模式?`)) { isEnabled = intendedState; GM_setValue('enableWideScreen', isEnabled); // 保存设置 registerMenuCommand(); // 更新菜单显示 if (isEnabled) { // 如果开启自动宽屏 ensureWideMode(); // 确保进入宽屏模式 } else { // 如果关闭自动宽屏 // 如果当前处于宽屏模式,则模拟点击宽屏按钮退出宽屏 if (elements.wideBtn && elements.wideBtn.classList.contains('bpx-state-entered')) { elements.wideBtn.click(); } setTimeout(checkAndScroll, 100); // 稍作延迟后检查滚动状态 } } } /** 处理宽屏按钮被点击的事件。 */ function handleWideBtnClick() { checkAndScroll(); // 立即检查一次 setTimeout(checkAndScroll, 200); // 延迟后再次检查,确保状态更新 } /** * 核心初始化逻辑:尝试缓存元素,如果失败则使用 MutationObserver 等待元素加载。 */ function initializeScriptLogic() { reInitScheduled = false; // 重置重新初始化计划标志 clearTimeout(initTimeout); // 清除可能存在的初始化延迟计时器 // 清理任何可能存在的旧 Observer 和其超时 if (coreElementsObserver) { coreElementsObserver.disconnect(); coreElementsObserver = null; } if (observerTimeoutId) { clearTimeout(observerTimeoutId); observerTimeoutId = null; } console.log("[B站自动宽屏居中] initializeScriptLogic: 开始初始化脚本逻辑..."); // 1. 尝试立即缓存核心元素 if (cacheElements()) { console.log("[B站自动宽屏居中] initializeScriptLogic: 核心元素已通过初次尝试成功缓存。"); setupListeners(); // 设置事件监听 if (isEnabled) ensureWideMode(); // 如果启用,则确保宽屏 setTimeout(checkAndScroll, FINAL_CHECK_DELAY); // 最终状态检查 return; // 初始化成功,结束 } // 2. 如果初次缓存失败,则设置 MutationObserver 等待元素出现 console.log("[B站自动宽屏居中] initializeScriptLogic: 初次缓存失败,设置 MutationObserver。"); const observerCallback = function(mutationsList, observerInstance) { // 检查核心元素(播放器和宽屏按钮)是否已出现在DOM中 if (document.querySelector('#bilibili-player') && document.querySelector('.bpx-player-ctrl-wide')) { console.log("[B站自动宽屏居中] MutationObserver 检测到变化,尝试重新缓存元素。"); if (cacheElements()) { // 再次尝试缓存 console.log("[B站自动宽屏居中] MutationObserver 触发: 核心元素已成功缓存。"); observerInstance.disconnect(); // 成功找到,停止观察 clearTimeout(observerTimeoutId); // 清除安全超时 coreElementsObserver = null; // 清理 observer 实例变量 observerTimeoutId = null; setupListeners(); // 设置事件监听 if (isEnabled) ensureWideMode(); // 如果启用,则确保宽屏 setTimeout(checkAndScroll, FINAL_CHECK_DELAY); // 最终状态检查 } else { // 此情况理论上较少发生:querySelector 找到了基本元素,但 cacheElements 内部的完整检查仍失败 console.warn("[B站自动宽屏居中] MutationObserver 触发: querySelector 找到基本元素,但 cacheElements 完整检查失败。"); } } }; coreElementsObserver = new MutationObserver(observerCallback); // 选择一个合适的父节点进行观察,目标是尽早发现播放器相关元素的注入 // 尝试的顺序:#playerWrap -> #mirror-vdcon -> #app -> document.body let targetNodeToObserve = document.getElementById('playerWrap') || document.getElementById('mirror-vdcon') || document.getElementById('app') || document.body; // 最差情况观察整个 body console.log("[B站自动宽屏居中] MutationObserver 将开始观察节点:", targetNodeToObserve.id || targetNodeToObserve.tagName); coreElementsObserver.observe(targetNodeToObserve, { childList: true, subtree: true }); // 设置一个最长等待超时,防止 MutationObserver 无限期运行 observerTimeoutId = setTimeout(() => { if (coreElementsObserver) { // 检查 Observer 是否仍然存在 (可能已被成功回调清除) console.error(`[B站自动宽屏居中] MutationObserver 超时 (${OBSERVER_MAX_WAIT_TIME}ms): 未能找到或缓存核心元素。`); coreElementsObserver.disconnect(); coreElementsObserver = null; } }, OBSERVER_MAX_WAIT_TIME); } /** * 安排脚本的重新初始化,通常在检测到页面导航(URL路径变化)时调用。 * @param {number} delay 重新初始化前的延迟时间 (ms) */ function scheduleReInitialization(delay = URL_CHECK_DELAY) { if (reInitScheduled) return; // 如果已安排,则不再重复安排 reInitScheduled = true; clearTimeout(initTimeout); // 清除之前的延迟计时器 console.log(`[B站自动宽屏居中] scheduleReInitialization: 安排在 ${delay}ms 后重新初始化。`); initTimeout = setTimeout(() => { removeListenersAndObserver(); // 清理旧的监听器和状态 setTimeout(initializeScriptLogic, 100); // 短暂延迟后开始新的初始化 }, delay); } /** * 检查给定的URL是否匹配脚本的目标页面规则。 * @param {string} url 要检查的URL * @returns {boolean} 如果是目标页面则返回true,否则返回false */ function isTargetPage(url) { return /\/(video|list|bangumi\/play)\//.test(url); // 匹配视频页、列表页、番剧播放页 } /** * 处理URL发生变化(包括SPA导航和历史记录变化)。 * 主要通过比较URL的pathname部分来判断是否需要重新初始化脚本。 */ function handleUrlChange() { // 使用 requestAnimationFrame 确保在浏览器下一次重绘前执行,优化性能 requestAnimationFrame(() => { const newHref = window.location.href; const newPathname = window.location.pathname; let oldPathnameFromCurrentUrl = '/'; // 默认值,以防 currentUrl 解析失败 if (currentUrl) { // 确保 currentUrl 有值 try { // currentUrl 存储的是上一次检查时的完整 href oldPathnameFromCurrentUrl = new URL(currentUrl).pathname; } catch (e) { console.warn('[B站自动宽屏居中] 解析旧URL的pathname失败:', currentUrl, e); // 备用方案:尝试从旧的完整URL中提取路径部分 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); } } } } // 仅当 URL 的路径部分 (pathname) 发生变化时,才认为需要重新初始化 if (newPathname !== oldPathnameFromCurrentUrl) { console.log(`[B站自动宽屏居中] Pathname 变化: 从 "${oldPathnameFromCurrentUrl}" 到 "${newPathname}". 触发重新初始化.`); const previousFullUrl = currentUrl; // 保存变化前的完整URL,用于isTargetPage判断 currentUrl = newHref; // 更新当前URL记录为新的完整URL const wasTarget = isTargetPage(previousFullUrl); const isNowTarget = isTargetPage(newHref); if (isNowTarget) { // 如果新URL是目标页面 scheduleReInitialization(); } else if (wasTarget && !isNowTarget) { // 如果从目标页面导航到非目标页面 console.log("[B站自动宽屏居中] 从目标页面导航到非目标页面 (Pathname change),移除监听器。"); removeListenersAndObserver(); clearTimeout(initTimeout); // 取消任何待处理的重新初始化 reInitScheduled = false; } } else if (newHref !== currentUrl) { // Pathname 未变,但完整 URL (可能查询参数或哈希值) 变了。 // 此时仅更新 currentUrl 记录,不触发重新初始化。 // console.log(`[B站自动宽屏居中] URL (query/hash) 变化,Pathname 未变. 不重新初始化.`); // 调试时可取消注释 currentUrl = newHref; } }); } /** 脚本主入口函数。 */ function main() { console.log(`[B站自动宽屏居中] 脚本开始执行 (main)。版本: ${SCRIPT_VERSION}`); isEnabled = GM_getValue('enableWideScreen', false); // 读取用户保存的设置 registerMenuCommand(); // 注册油猴菜单 // 监听浏览器历史记录变化 (前进/后退按钮) window.addEventListener('popstate', handleUrlChange); // 劫持 history.pushState 和 history.replaceState 以监听SPA导航 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; }; // 监听自定义的 history state 变化事件 window.addEventListener('historystatechanged', handleUrlChange); // 初始加载时,如果当前页面是目标页面,则初始化脚本 if (isTargetPage(currentUrl)) { // currentUrl 已在顶部初始化 initializeScriptLogic(); } // 页面卸载时执行清理操作 window.addEventListener('unload', () => { removeListenersAndObserver(); history.pushState = originalPushState; history.replaceState = originalReplaceState; window.removeEventListener('historystatechanged', handleUrlChange); window.removeEventListener('popstate', handleUrlChange); clearTimeout(initTimeout); }); } // --- 启动脚本 --- // 等待 DOM 内容加载完成后执行 main 函数 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); // 如果 DOM 已加载,则直接执行 } })();