// ==UserScript==
// @id BilibiliWatchlaterButton@Laster2800
// @name B站“稍后再看”按钮
// @version 1.3.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=*
// @include *://t.bilibili.com/?tab=*
// @exclude *://message.bilibili.com/pages/*
// ==/UserScript==
(function() {
// 新版顶栏中加入“稍后再看”的按钮
executeAfterElementLoad({
selector: '.user-con.signin',
callback: addHeaderWatchlaterButton,
})
if (/bilibili.com\/video(|\/.*)$/.test(location.href)) {
// 将视频播放页中隐藏在弹出菜单中的“稍后再看”按钮移出来
executeAfterConditionPass({
condition: () => {
// 必须在确定 Vue 加载完成后再修改 DOM 结构,否则会导致 Vue 加载出错造成页面错误
var app = document.querySelector('#app')
var vueLoad = app && 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.style.margin = '0 2px 2px 0'
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)),
})
}