// ==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 = `