// ==UserScript== // @name BiliPlus - Bilibili 加大杯 // @namespace https://github.com/timothy-lau/biliplus // @version 1.0.5 // @description 专门为细节控的人群使用,只要在B站上设计不合理的地方,都可以加入到大杯中。 // @author timothy-lau@outlook.com // @match https://www.bilibili.com/* // @match https://*.bilibili.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // ==================== 默认配置 ==================== const DEFAULT_SETTINGS = { 'biliplus-enable': true, 'clean-home-page': true, 'feed-roll-history-btn': true, 'stepless-video-rate': true, 'hide-hot-search-list': true }; // 获取设置 function getSetting(key) { const value = GM_getValue(key); return value !== undefined ? value : DEFAULT_SETTINGS[key]; } // 保存设置 function setSetting(key, value) { GM_setValue(key, value); } // ==================== 工具库 ==================== class _UTILS { // 从 URL 中提取 bvid static getBvidFromUrl(url) { const match = /\/video\/([A-Za-z0-9]+)/.exec(url); if (match) { return match[1]; } return null; } // 查找满足条件的父元素 static findParentElement(element, func) { let _pe = element.parentElement; while (_pe != null) { if (func(_pe)) { return _pe; } _pe = _pe.parentElement; } return null; } // WBI 签名相关 static mixinKeyEncTab = [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52 ]; static getMixinKey = (orig) => { return this.mixinKeyEncTab.map((n) => orig[n]).join("").slice(0, 32); }; static encWbi(params, img_key, sub_key) { const mixin_key = this.getMixinKey(img_key + sub_key); const curr_time = Math.round(Date.now() / 1000); const chr_filter = /[!'()*]/g; Object.assign(params, { wts: curr_time }); const query = Object.keys(params).sort().map((key) => { const value = params[key].toString().replace(chr_filter, ""); return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; }).join("&"); const wbi_sign = md5(query + mixin_key); return query + "&w_rid=" + wbi_sign; } static async getWbiKeys() { const { wbi_img: { img_url, sub_url } } = await _BILIAPI.getNavUserInfo(); return { img_key: img_url.slice(img_url.lastIndexOf("/") + 1, img_url.lastIndexOf(".")), sub_key: sub_url.slice(sub_url.lastIndexOf("/") + 1, sub_url.lastIndexOf(".")) }; } static async getwts(params) { const web_keys = await this.getWbiKeys(); return this.encWbi(params, web_keys.img_key, web_keys.sub_key); } // MutationObserver 辅助函数 static observe(node, callback, options) { const observer = new MutationObserver((mutations, ob) => { callback(mutations, ob); }); observer.observe(node, Object.assign({ childList: true, subtree: true }, options)); return () => observer.disconnect(); } } // ==================== B站 API ==================== class _BILIAPI { static BILIBILI_API = 'https://api.bilibili.com'; // 获取视频信息 static async getVideoInfo(bvid) { const response = await fetch(`${this.BILIBILI_API}/x/web-interface/view?bvid=${bvid}`); const jsonData = await response.json(); if (response.status !== 200 || !jsonData) { throw new Error(); } return jsonData.data; } // 获取导航栏用户信息(含 WBI 密钥) static async getNavUserInfo() { const response = await fetch(`${this.BILIBILI_API}/x/web-interface/nav`); const jsonData = await response.json(); if (response.status !== 200 || !jsonData) { throw new Error(); } return jsonData.data; } } // ==================== 注入样式 ==================== GM_addStyle(` .biliplus-disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; } .biliplus-load-more-anchor { position: absolute; opacity: 0; top: 200px; } .biliplus-hide-hot-search-list .search-panel .trending { display: none; } .biliplus-hide-hot-search-list.biliplus-hide-hot-search-list-search-panel-raduis .center-search__bar.is-focus #nav-searchform.is-focus { border-radius: 8px !important; } .biliplus-stepless-video-rate .bpx-player-ctrl-btn.bpx-player-ctrl-playbackrate { display: none; } .feed-roll-back-btn { flex-direction: column; margin-left: 0 !important; height: 40px !important; width: 40px; padding: 9px; margin-top: 6px; } .feed-roll-back-btn svg { margin-right: 0; margin-bottom: 0px; } .feed-roll-next-btn { flex-direction: column; margin-left: 0 !important; height: 40px !important; width: 40px; padding: 9px; margin-top: 6px; } .feed-roll-next-btn svg { margin-right: 0; margin-bottom: 0px; } body[biliplus-clean-mode] .recommended-container_floor-aside .container > *:nth-of-type(n + 8) { margin-top: 0px !important; } body[biliplus-clean-mode] .recommended-container_floor-aside .container.is-version8 > *:nth-of-type(n + 13) { margin-top: 0px !important; } /* 无级倍速按钮样式 */ .stepless-video-rate-btn { fill: #fff; color: hsla(0, 0%, 100%, 0.8); height: 22px; line-height: 22px; outline: 0; position: relative; text-align: center; z-index: 2; font-size: 14px; width: auto; margin-right: 10px; } .stepless-video-rate-btn:hover { color: #fff; } .stepless-video-rate-btn-result { cursor: pointer; font-weight: 600; width: 100%; user-select: none; } .stepless-video-rate-box { background: hsla(0, 0%, 8%, 0.9); border-radius: 2px; bottom: 41px; display: none; height: 148px; left: 50%; margin-left: -16px; position: absolute; width: 32px; flex-direction: column; justify-content: space-between; align-items: center; padding: 4px 0; box-sizing: border-box; } .stepless-video-rate-box.display { display: flex; } .stepless-video-rate-arrow { display: flex; justify-content: center; align-items: center; height: 24px; width: 100%; color: #e5e9ef; cursor: pointer; user-select: none; transition: color 0.2s, transform 0.1s; flex-shrink: 0; } .stepless-video-rate-arrow:hover { color: #00aeec; } .stepless-video-rate-arrow:active { transform: scale(0.9); } .stepless-video-rate-arrow svg { width: 16px; height: 16px; } .stepless-video-rate-number { color: #e5e9ef; font-size: 12px; height: 24px; line-height: 24px; text-align: center; width: 100%; flex-shrink: 0; } .stepless-video-rate-progress { height: 60px !important; margin: 0 auto; flex-shrink: 0; } .stepless-video-rate-progress .bui-area { -webkit-box-pack: center !important; -ms-flex-pack: center !important; justify-content: center !important; } /* 禁用原生滚动条的交互 */ .stepless-video-rate-progress .bui-thumb { pointer-events: none; } .stepless-video-rate-progress .bui-track { pointer-events: none; } @media screen and (min-width: 750px) { .bpx-player-container[data-screen='full'] .stepless-video-rate-btn, .bpx-player-container[data-screen='web'] .stepless-video-rate-btn { height: 43px; line-height: 32px; font-size: 16px; } .bpx-player-container[data-screen='full'] .stepless-video-rate-box, .bpx-player-container[data-screen='web'] .stepless-video-rate-box { bottom: 64px; } } /* 设置面板样式 */ .biliplus-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 99999; display: flex; justify-content: center; align-items: center; } .biliplus-settings-panel { background: #fff; border-radius: 12px; padding: 24px; width: 400px; max-width: 90%; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .biliplus-settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #e3e5e7; } .biliplus-settings-title { font-size: 18px; font-weight: 600; color: #18191c; margin: 0; } .biliplus-settings-close { background: none; border: none; font-size: 24px; color: #9499a0; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: all 0.2s; } .biliplus-settings-close:hover { background: #f1f2f3; color: #18191c; } .biliplus-settings-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f2f3; } .biliplus-settings-item:last-child { border-bottom: none; } .biliplus-settings-item-label { font-size: 14px; color: #18191c; font-weight: 500; } .biliplus-settings-item-desc { font-size: 12px; color: #9499a0; margin-top: 4px; } .biliplus-settings-switch { position: relative; width: 44px; height: 24px; background: #c9ccd0; border-radius: 12px; cursor: pointer; transition: background 0.3s; flex-shrink: 0; } .biliplus-settings-switch.active { background: #00aeec; } .biliplus-settings-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: #fff; border-radius: 50%; transition: transform 0.3s; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .biliplus-settings-switch.active::after { transform: translateX(20px); } .biliplus-settings-footer { margin-top: 20px; padding-top: 16px; border-top: 1px solid #e3e5e7; text-align: center; font-size: 12px; color: #9499a0; } `); // ==================== 全局补丁 ==================== window.addEventListener('keydown', e => { if (e.key === 'Escape') { const exitButton = document.querySelector('.reply-view-image .operation-btn-icon.close-container'); if (exitButton) { exitButton.click(); } } }); // ==================== 首页干净模式 ==================== function initCleanHomePage() { if (!getSetting('clean-home-page')) { return; } let body = document.getElementsByTagName('body')[0]; body.setAttribute('biliplus-clean-mode', ''); const recommendedSwipe = document.getElementsByClassName('recommended-swipe')[0]; if (recommendedSwipe) { recommendedSwipe.remove(); } const loadMoreAnchor = document.querySelector('.load-more-anchor'); if (loadMoreAnchor) { loadMoreAnchor.classList.add('biliplus-load-more-anchor'); const scroll = new Event('scroll'); dispatchEvent(scroll); loadMoreAnchor.classList.remove('biliplus-load-more-anchor'); } } // ==================== 首页"换一换"回溯功能 ==================== function initFeedRollHistoryBtn() { if (!getSetting('feed-roll-history-btn')) { return; } const feedHistory = []; let feedHistoryIndex = 0; const feedRollBackBtn = ` `; const feedRollNextBtn = ` `; const targetNode = document.querySelector('.recommended-container_floor-aside'); if (targetNode) { const disconnect = _UTILS.observe(targetNode, () => { let feedRollBtn = document.getElementsByClassName('roll-btn')[0]; if (feedRollBtn) { // 添加“后退”按钮 let backBtn = document.createElement('button'); feedRollBtn.parentNode.appendChild(backBtn); backBtn.outerHTML = feedRollBackBtn; document.getElementById('feed-roll-back-btn').addEventListener('click', () => { let feedCards = document.getElementsByClassName('feed-card'); if (feedHistoryIndex == feedHistory.length) { feedHistory.push(listInnerHTMLOfFeedCard(feedCards)); } for (let fc_i = 0; fc_i < feedCards.length; fc_i++) { feedCards[fc_i].innerHTML = feedHistory[feedHistoryIndex - 1][fc_i]; } feedHistoryIndex = feedHistoryIndex - 1; if (feedHistoryIndex == 0) { disableElementById('feed-roll-back-btn', true); } disableElementById('feed-roll-next-btn', false); }); // 添加“前进”按钮 let nextBtn = document.createElement('div'); feedRollBtn.parentNode.appendChild(nextBtn); nextBtn.outerHTML = feedRollNextBtn; document.getElementById('feed-roll-next-btn').addEventListener('click', () => { let feedCards = document.getElementsByClassName('feed-card'); for (let fc_i = 0; fc_i < feedCards.length; fc_i++) { feedCards[fc_i].innerHTML = feedHistory[feedHistoryIndex + 1][fc_i]; } feedHistoryIndex = feedHistoryIndex + 1; if (feedHistoryIndex == feedHistory.length - 1) { disableElementById('feed-roll-next-btn', true); } disableElementById('feed-roll-back-btn', false); }); // 监听“换一换”按钮点击 feedRollBtn.id = 'feed-roll-btn'; feedRollBtn.addEventListener('click', () => { setTimeout(() => { if (feedHistoryIndex == feedHistory.length) { feedHistory.push(listInnerHTMLOfFeedCard(document.getElementsByClassName('feed-card'))); } feedHistoryIndex = feedHistory.length; disableElementById('feed-roll-back-btn', false); disableElementById('feed-roll-next-btn', true); }); }); disconnect(); } }); } function disableElementById(id, bool) { const element = document.getElementById(id); if (element) { if (bool) { element.classList.add('biliplus-disabled'); } else { element.classList.remove('biliplus-disabled'); } } } function listInnerHTMLOfFeedCard(feedCardElements) { return Array.from(feedCardElements).map(fc => fc.innerHTML); } } // ==================== 无级视频倍速 ==================== function initSteplessVideoRate() { if (!getSetting('stepless-video-rate')) { return; } let videoRate = 1.0; let hideBoxTimeout = null; const rateButton = `
无级倍速
1.0
`; document.body.classList.add('biliplus-stepless-video-rate'); const disconnect = _UTILS.observe(document.body, () => { if (document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-playbackrate') == null) { return; } if (document.querySelector('.stepless-video-rate-btn') == null) { const playerControl = document.querySelector('.bpx-player-control-bottom-right'); const oldRateButton = document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-playbackrate'); if (playerControl && oldRateButton) { const newRateButton = document.createElement('div'); playerControl.insertBefore(newRateButton, oldRateButton); newRateButton.outerHTML = rateButton; const box = document.querySelector('.stepless-video-rate-box'); const dot = document.querySelector('.stepless-video-rate-box .bui-thumb'); const bar = document.querySelector('.stepless-video-rate-box .bui-bar'); const rate = document.querySelector('.stepless-video-rate-box .stepless-video-rate-number'); const upArrow = document.querySelector('.stepless-video-rate-up'); const downArrow = document.querySelector('.stepless-video-rate-down'); // 统一更新倍速与UI的函数 function updateRate(newRate) { videoRate = Math.max(0.1, Math.min(5.0, parseFloat(newRate.toFixed(1)))); rate.innerText = videoRate.toFixed(1); const video = document.querySelector('video'); if (video) { video.playbackRate = videoRate; } // 同步滚动条位置 (进度条高度60px,最大偏移48px) const positionY = -(videoRate / 5) * 48; dot.style.transform = `translateY(${positionY}px)`; bar.style.transform = `scaleY(${videoRate / 5})`; } // 长按逻辑 let pressTimer = null; let pressInterval = null; function startPress(direction) { let step = direction * 0.1; updateRate(videoRate + step); pressTimer = setTimeout(() => { pressInterval = setInterval(() => { updateRate(videoRate + step); }, 100); // 长按连续调节速度 }, 300); // 触发长按判定延迟 } function stopPress() { clearTimeout(pressTimer); clearInterval(pressInterval); pressTimer = null; pressInterval = null; } // 绑定上箭头事件 if (upArrow) { upArrow.addEventListener('mousedown', (e) => { e.preventDefault(); startPress(1); }); upArrow.addEventListener('mouseup', stopPress); upArrow.addEventListener('mouseleave', stopPress); upArrow.addEventListener('touchstart', (e) => { e.preventDefault(); startPress(1); }, { passive: false }); upArrow.addEventListener('touchend', stopPress); } // 绑定下箭头事件 if (downArrow) { downArrow.addEventListener('mousedown', (e) => { e.preventDefault(); startPress(-1); }); downArrow.addEventListener('mouseup', stopPress); downArrow.addEventListener('mouseleave', stopPress); downArrow.addEventListener('touchstart', (e) => { e.preventDefault(); startPress(-1); }, { passive: false }); downArrow.addEventListener('touchend', stopPress); } // 显示/隐藏 Box const bilibiliPlayer = document.querySelector('#bilibili-player'); if (bilibiliPlayer) { bilibiliPlayer.addEventListener('mouseover', e => { if (e.target.nodeName === 'DIV' && e.target.parentElement && e.target.parentElement.classList.contains('stepless-video-rate-btn')) { showBox(); if (hideBoxTimeout != null) { clearTimeout(hideBoxTimeout); } } }); } const steplessVideoRateBtn = document.querySelector('.stepless-video-rate-btn'); if (steplessVideoRateBtn) { steplessVideoRateBtn.addEventListener('mouseleave', () => { hideBoxTimeout = setTimeout(hideBox, 400); }); } // 双击重置倍速 const steplessBtn = document.querySelector('.stepless-video-rate-btn-result'); if (steplessBtn) { steplessBtn.addEventListener('dblclick', () => { updateRate(1.0); }); } } } else { disconnect(); } }); function showBox() { const rateBox = document.querySelector('.stepless-video-rate-box'); if (rateBox && !rateBox.classList.contains('display')) { rateBox.classList.add('display'); } } function hideBox() { const rateBox = document.querySelector('.stepless-video-rate-box'); if (rateBox && rateBox.classList.contains('display')) { rateBox.classList.remove('display'); } } } // ==================== 隐藏搜索栏热搜列表 ==================== function initHideHotSearchList() { if (!getSetting('hide-hot-search-list')) { return; } const body = document.querySelector('body'); body.classList.add('biliplus-hide-hot-search-list'); const navSearchform = document.querySelector('#nav-searchform'); if (navSearchform) { navSearchform.addEventListener('focusin', () => { const history = document.querySelector('.search-panel .history'); const searchPanel = document.querySelector('.search-panel'); if (!history) { if (searchPanel) { searchPanel.style.display = 'none'; body.classList.add('biliplus-hide-hot-search-list-search-panel-raduis'); } } else { if (searchPanel) { searchPanel.style.display = 'block'; body.classList.remove('biliplus-hide-hot-search-list-search-panel-raduis'); const clearButton = document.querySelector('.search-panel .history .clear'); if (clearButton) { clearButton.addEventListener('click', () => { searchPanel.style.display = 'none'; body.classList.add('biliplus-hide-hot-search-list-search-panel-raduis'); }); } } } }); } const navSearchInput = document.querySelector('.nav-search-input'); if (navSearchInput) { navSearchInput.addEventListener('input', () => { const suggestions = document.querySelector('.search-panel .suggestions'); const history = document.querySelector('.search-panel .history'); const searchPanel = document.querySelector('.search-panel'); if (!suggestions && !history) { if (searchPanel) { searchPanel.style.display = 'none'; body.classList.add('biliplus-hide-hot-search-list-search-panel-raduis'); } } else { if (searchPanel) { searchPanel.style.display = 'block'; body.classList.remove('biliplus-hide-hot-search-list-search-panel-raduis'); } } }); } } // ==================== 设置面板 ==================== function createSettingsPanel() { if (document.getElementById('biliplus-settings-overlay')) { return; } const overlay = document.createElement('div'); overlay.id = 'biliplus-settings-overlay'; overlay.className = 'biliplus-settings-overlay'; const panel = document.createElement('div'); panel.className = 'biliplus-settings-panel'; const header = document.createElement('div'); header.className = 'biliplus-settings-header'; const title = document.createElement('h2'); title.className = 'biliplus-settings-title'; title.textContent = 'BiliPlus 设置'; const closeBtn = document.createElement('button'); closeBtn.className = 'biliplus-settings-close'; closeBtn.innerHTML = '×'; closeBtn.addEventListener('click', closeSettingsPanel); header.appendChild(title); header.appendChild(closeBtn); // 设置项 const settingsItems = [ { key: 'biliplus-enable', label: 'BiliPlus 总开关', desc: '关闭后所有功能不生效' }, { key: 'clean-home-page', label: '首页干净模式', desc: '移除首页推荐区的滑动条,优化布局' }, { key: 'feed-roll-history-btn', label: '首页"换一换"回溯', desc: '在首页添加前后翻页按钮,可查看之前的推荐内容' }, { key: 'stepless-video-rate', label: '无级视频倍速', desc: '支持通过上下箭头点击步进0.1、长按连续调节' }, { key: 'hide-hot-search-list', label: '隐藏热搜列表', desc: '隐藏搜索框下方的热搜内容' } ]; const footer = document.createElement('div'); footer.className = 'biliplus-settings-footer'; footer.textContent = '修改设置后请刷新页面生效'; panel.appendChild(header); settingsItems.forEach(item => { const settingItem = document.createElement('div'); settingItem.className = 'biliplus-settings-item'; const labelDiv = document.createElement('div'); const label = document.createElement('div'); label.className = 'biliplus-settings-item-label'; label.textContent = item.label; const desc = document.createElement('div'); desc.className = 'biliplus-settings-item-desc'; desc.textContent = item.desc; labelDiv.appendChild(label); labelDiv.appendChild(desc); const switchBtn = document.createElement('div'); switchBtn.className = 'biliplus-settings-switch' + (getSetting(item.key) ? ' active' : ''); switchBtn.dataset.key = item.key; switchBtn.addEventListener('click', () => { const isActive = switchBtn.classList.contains('active'); switchBtn.classList.toggle('active'); setSetting(item.key, !isActive); }); settingItem.appendChild(labelDiv); settingItem.appendChild(switchBtn); panel.appendChild(settingItem); }); panel.appendChild(footer); overlay.appendChild(panel); document.body.appendChild(overlay); overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeSettingsPanel(); } }); const escHandler = (e) => { if (e.key === 'Escape') { closeSettingsPanel(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } function closeSettingsPanel() { const overlay = document.getElementById('biliplus-settings-overlay'); if (overlay) { overlay.remove(); } } // 注册油猴菜单命令 if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('⚙️ BiliPlus 设置', createSettingsPanel); } // ==================== 初始化入口 ==================== function initBiliPlus() { if (!getSetting('biliplus-enable')) { return; } // 首页功能 if (window.location.hostname === 'www.bilibili.com' && window.location.pathname === '/') { initFeedRollHistoryBtn(); initCleanHomePage(); } // 全局功能 initHideHotSearchList(); // 视频页功能 if (window.location.pathname.includes('/video/') || window.location.pathname.includes('/list/') || window.location.pathname.includes('/bangumi/play/')) { initSteplessVideoRate(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initBiliPlus); } else { initBiliPlus(); } // 监听 SPA 路由变化 let lastUrl = location.href; new MutationObserver(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; initBiliPlus(); } }).observe(document, { subtree: true, childList: true }); })();