// ==UserScript== // @id BilibiliWatchlaterButton@Laster2800 // @name B站“稍后再看”按钮 // @version 1.3 // @namespace laster2800 // @author Laster2800 // @description B站新版顶栏中加回“稍后再看”的按钮,并将视频播放页中隐藏在弹出菜单中的“稍后再看”按钮移出来 // @include *://www.bilibili.com/* // @include *://message.bilibili.com/* // @include *://search.bilibili.com/* // @include *://space.bilibili.com/* // @include *://t.bilibili.com/ // @include *://t.bilibili.com/?spm_id_from* // @exclude *://message.bilibili.com/pages/* // @downloadURL none // ==/UserScript== (function() { // 新版顶栏中加入“稍后再看”的按钮 executeAfterElementLoad({ selector: '.user-con.signin', callback: addHeaderWatchlaterButton, }) if (/bilibili.com\/video(|\/.*)$/.test(location.href)) { // 将视频播放页中隐藏在弹出菜单中的“稍后再看”按钮移出来 executeAfterConditionPass({ condition: () => { // 必须在确定 Vue 加载完成后再修改 DOM 结构,否则会导致 Vue 加载出错造成页面错误 var vueLoad = document.querySelector('#app').__vue__ if (!vueLoad) { return false } var atr = document.querySelector('#arc_toolbar_report') var original = atr && atr.querySelector('.van-watchlater') if (original && original.__vue__) { return [atr, original] } else { return false } }, callback: addVideoWatchlaterButton, }) } })(); function addHeaderWatchlaterButton(header) { if (header) { var collect = header.children[4] var watchlater = header.children[6].cloneNode(true) var link = watchlater.firstChild link.href = 'https://www.bilibili.com/watchlater/#/list' var text = link.firstChild text.innerText = '稍后再看' header.insertBefore(watchlater, collect) // 鼠标移动到“稍后再看”按钮上时,以 Tooltip 形式显示“稍后再看”列表 var watchlaterPanelSelector = '[role=tooltip][aria-hidden=false] .tabs-panel [title=稍后再看]' var dispVue = collect.firstChild.__vue__ watchlater.onmouseover = () => { // 确保原列表完全消失后再显示,避免从“收藏”移动到“稍后再看”时列表反而消失的问题 executeAfterConditionPass({ condition: () => !document.querySelector(watchlaterPanelSelector), callback: () => { dispVue.showPopper = true executeAfterElementLoad({ selector: watchlaterPanelSelector, callback: watchlaterPanel => watchlaterPanel.parentNode.click(), interval: 50, timeout: 1500, }) }, interval: 10, timeout: 500, }) } // 鼠标从“稍后再看”离开时关闭列表,但移动到“收藏”上面时不关闭 collect.onmouseover = () => { collect.mouseOver = true } collect.onmouseleave = () => { collect.mouseOver = false } watchlater.onmouseleave = () => { // 要留出足够空间让 collect.mouseOver 变化 // 但有时候还是会闪,毕竟常规方式估计是无法阻止鼠标移动到“收藏”上时的 Vue 事件 setTimeout(() => { if (!collect.mouseOver) { dispVue.showPopper = false } }, 100) } } } function addVideoWatchlaterButton([atr, original]) { var btn = document.createElement('label') var cb = document.createElement('input') cb.type = 'checkbox' cb.style.verticalAlign = 'middle' cb.checked = original.__vue__.added // 第一次取到的值总是 false,不知道是不是B站的 BUG btn.appendChild(cb) var text = document.createElement('span') text.innerText = '稍后再看' btn.className = 'appeal-text' cb.onclick = () => { // 不要附加到 btn 上,否则点击时会执行两次 var oVue = original.__vue__ oVue.handler() var checked = !oVue.added // 检测操作是否生效,失败时弹出提示 executeAfterConditionPass({ condition: () => checked === oVue.added, callback: () => { cb.checked = checked }, interval: 50, timeout: 500, onTimeout: () => { cb.checked = oVue.added alert(checked ? '添加至稍后再看失败' : '从稍后再看移除失败') }, }) } btn.title = '目前B站在第一次打开视频时,内部状态总是显示该视频还没有加入稍后再看,此为B站的 BUG,并非本脚本的锅。' btn.appendChild(text) atr.appendChild(btn) original.parentNode.style.display = 'none' } /** * 在条件满足后执行操作 * * 当条件满足后,如果不存在终止条件,那么直接执行 callback(result)。 * * 当条件满足后,如果存在终止条件,且 stopTimeout 大于 0,则还会在接下来的 stopTimeout 时间内判断是否满足终止条件,称为终止条件的二次判断。 * 如果在此期间,终止条件通过,则表示依然不满足条件,故执行 stopCallback() 而非 callback(result)。 * 如果在此期间,终止条件一直失败,则顺利通过检测,执行 callback(result)。 * * @param {Object} [options={}] 选项 * @param {Function} [options.condition] 条件,当 condition() 返回的 result 为真值时满足条件 * @param {Function} [options.callback] 当满足条件时执行 callback(result) * @param {number} [options.interval=100] 检测时间间隔(单位:ms) * @param {number} [options.timeout=5000] 检测超时时间,检测时间超过该值时终止检测(单位:ms) * @param {Function} [options.onTimeout] 检测超时时执行 onTimeout() * @param {Function} [options.stopCondition] 终止条件,当 stopCondition() 返回的 stopResult 为真值时终止检测 * @param {Function} [options.stopCallback] 终止条件达成时执行 stopCallback()(包括终止条件的二次判断达成) * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms) * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms) */ function executeAfterConditionPass(options) { var defaultOptions = { condition: () => true, callback: result => console.log(result), interval: 100, timeout: 5000, onTimeout: null, stopCondition: null, stopCallback: null, stopInterval: 50, stopTimeout: 0, } var o = { ...defaultOptions, ...options } if (!o.callback instanceof Function) { return } var cnt = 0 var maxCnt = o.timeout / o.interval var tid = setInterval(() => { var result = o.condition() var stopResult = o.stopCondition && o.stopCondition() if (stopResult) { clearInterval(tid) o.stopCallback instanceof Function && o.stopCallback() } else if (++cnt > maxCnt) { clearInterval(tid) o.onTimeout instanceof Function && o.onTimeout() } else if (result) { clearInterval(tid) if (o.stopCondition && o.stopTimeout > 0) { executeAfterConditionPass({ condition: o.stopCondition, callback: o.stopCallback, interval: o.stopInterval, timeout: o.stopTimeout, onTimeout: () => o.callback(result) }) } else { o.callback(result) } } }, o.interval) } /** * 在元素加载完成后执行操作 * * 当元素加载成功后,如果没有设置终止元素选择器,那么直接执行 callback(element)。 * * 当元素加载成功后,如果没有设置终止元素选择器,且 stopTimeout 大于 0,则还会在接下来的 stopTimeout 时间内判断终止元素是否加载成功,称为终止元素的二次加载。 * 如果在此期间,终止元素加载成功,则表示依然不满足条件,故执行 stopCallback() 而非 callback(element)。 * 如果在此期间,终止元素加载失败,则顺利通过检测,执行 callback(element)。 * * @param {Object} [options={}] 选项 * @param {Function} [options.selector] 该选择器指定要等待加载的元素 element * @param {Function} [options.callback] 当 element 加载成功时执行 callback(element) * @param {number} [options.interval=100] 检测时间间隔(单位:ms) * @param {number} [options.timeout=5000] 检测超时时间,检测时间超过该值时终止检测(单位:ms) * @param {Function} [options.onTimeout] 检测超时时执行 onTimeout() * @param {Function} [options.stopCondition] 该选择器指定终止元素 stopElement,若该元素加载成功则终止检测 * @param {Function} [options.stopCallback] 终止元素加载成功后执行 stopCallback()(包括终止元素的二次加载) * @param {number} [options.stopInterval=50] 终止元素二次加载期间的检测时间间隔(单位:ms) * @param {number} [options.stopTimeout=0] 终止元素二次加载期间的检测超时时间(单位:ms) */ function executeAfterElementLoad(options) { var defaultOptions = { selector: '', callback: el => console.log(el), interval: 100, timeout: 5000, onTimeout: null, stopSelector: null, stopCallback: null, stopInterval: 50, stopTimeout: 0, } var o = { ...defaultOptions, ...options } executeAfterConditionPass({ ...o, condition: () => document.querySelector(o.selector), stopCondition: o.stopSelector && (() => document.querySelector(o.stopSelector)), }) }