Greasy Fork

Greasy Fork is available in English.

B站“稍后再看”按钮

B站新版顶栏中加回“稍后再看”的按钮

当前为 2020-01-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @id             BilibiliWatchlaterButton@Laster2800
// @name           B站“稍后再看”按钮
// @version        1.2.1
// @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/*
// ==/UserScript==

(function() {
    executeAfterElementLoaded({
        selector: '.user-con.signin',
        callback: addWatchlater,
    })
})();

function addWatchlater(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
                    executeAfterElementLoaded({
                        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)
        }
    }
}

/**
 * 在条件满足后执行操作
 *
 * 当条件满足后,如果不存在终止条件,那么直接执行 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 executeAfterElementLoaded(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)),
    })
}