您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
B站“稍后再看”功能增强,目前功能包括顶栏和播放页中相关UI、重定向至常规播放页、防误删记录等,支持功能设置
当前为
// ==UserScript== // @id BilibiliWatchlaterPlus@Laster2800 // @name B站“稍后再看”功能增强 // @version 2.0 // @namespace laster2800 // @author Laster2800 // @description B站“稍后再看”功能增强,目前功能包括顶栏和播放页中相关UI、重定向至常规播放页、防误删记录等,支持功能设置 // @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/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @connect api.bilibili.com // @run-at document-start // ==/UserScript== (function() { // 用户配置读取 var init = GM_getValue('gm395456') var configUpdate = 20200713 var headerButton = true var videoButton = true var redirect = false var openInNew = false var watchlaterHistory = false if (init >= configUpdate) { headerButton = GM_getValue('gm395456_headerButton') videoButton = GM_getValue('gm395456_videoButton') redirect = GM_getValue('gm395456_redirect') openInNew = GM_getValue('gm395456_openInNew') watchlaterHistory = GM_getValue('gm395456_watchlaterHistory') } else { init = false GM_setValue('gm395456_headerButton', headerButton) GM_setValue('gm395456_videoButton', videoButton) GM_setValue('gm395456_redirect', redirect) GM_setValue('gm395456_openInNew', openInNew) GM_setValue('gm395456_watchlaterHistory', watchlaterHistory) } // 重定向,document-start 就执行,尽可能快地将原页面掩盖过去 if (redirect && /bilibili.com\/medialist\/play\/watchlater\//.test(location.href)) { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/v2/history/toview/web?jsonp=jsonp`, onload: function(response) { if (response && response.responseText) { try { var part = parseInt(location.href.match(/(?<=\/watchlater\/p)\d+(?=\/?)/)[0]) var json = JSON.parse(response.responseText) var watchList = json.data.list location.replace('https://www.bilibili.com/video/' + watchList[part - 1].bvid) } catch(e) { console.error('重定向错误,请联系脚本作者( http://greasyfork.icu/zh-CN/scripts/383441/feedback ):\n' + e) } } } }) } // 脚本的其他部分推迟至 DOMContentLoaded 执行 document.addEventListener('DOMContentLoaded', () => { GM_addStyle(` #gm395456 .gm_setting { font-size: 12px; } #gm395456 .gm_setting .gm_settingPage { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #ffffff; border: 1px solid #ccd0d7; border-radius: 10px; z-index: 65535; } #gm395456 .gm_setting .gm_items { margin: 0 2em; font-size: 1.1em; } #gm395456 .gm_setting .gm_item { cursor: pointer; display: block; padding: 0.3em; } #gm395456 .gm_setting .gm_item:hover { color: #0075FF; } #gm395456 .gm_setting .gm_item input[type=checkbox] { vertical-align: middle; margin: 3px 0 0 10px; float: right; } #gm395456 .gm_setting .gm_bottom { margin: 1.2em 2em; text-align: center; } #gm395456 .gm_setting .gm_bottom button { font-size: 1em; padding: 0.2em 0.5em; margin: 0 0.4em; cursor: pointer; } #gm395456 .gm_history .gm_historyPage { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #ffffff; border: 1px solid #ccd0d7; border-radius: 10px; z-index: 65535; height: 75%; width: 60%; overflow-y: auto; } #gm395456 .gm_history .gm_historyPage::-webkit-scrollbar { display: none; } #gm395456 .gm_history .gm_comment { margin: 0 2em; text-align: center; color: gray; } #gm395456 .gm_history .gm_content { margin: 1.5em; font-size: 1.2em; text-align: center; } #gm395456 .gm_title{ font-size: 1.6em; margin: 0.8em; text-align: center; } #gm395456 .gm_shadow { background: #000000b0; position: fixed; top: 0%; left: 0%; z-index: 10000; width: 100%; height: 100%; } `) var el_gm395456 = document.body.appendChild(document.createElement('div')) el_gm395456.id = 'gm395456' var el_setting = null var el_history = null // 用户配置设置 GM_registerMenuCommand('设置', () => openUserSetting(el_gm395456, el_setting)) if (!init) { openUserSetting(el_gm395456, el_setting, true) } // 正式开始处理 if (headerButton) { // 顶栏中加入“稍后再看”入口 executeAfterElementLoad({ selector: '.user-con.signin', callback: addHeaderWatchlaterButton, }) } if (videoButton && /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, }) } else if (/bilibili.com\/watchlater\/.*\/list/.test(location.href)) { if (openInNew) { switchOpenInNew() // 新标签页打开 } if (watchlaterHistory) { var wHistory = [] // 将此时的稍后再看列表保存起来 GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/v2/history/toview/web?jsonp=jsonp`, onload: function(response) { if (response && response.responseText) { try { var json = JSON.parse(response.responseText) var watchList = json.data.list for (var e of watchList) { wHistory.push(`${e.title}<br>AV: ${e.aid}, BV: ${e.bvid}`) } } catch(e) { console.error('保存稍后再看列表错误,请联系脚本作者( http://greasyfork.icu/zh-CN/scripts/383441/feedback ):\n' + e) } } } }) // 防误删记录 GM_registerMenuCommand('显示防误删记录', () => openWatchlaterHistory(el_gm395456, el_history, wHistory)) } } // 打开用户设置 function openUserSetting(parent, el, initial) { if (el) { el.style.display = '' } else { el = parent.appendChild(document.createElement('div')) el.className = 'gm_setting' el.innerHTML = ` <div class="gm_settingPage"> <div class="gm_title">B站“稍后再看”功能增强</div> <div class="gm_items"> <label class="gm_item" title="在顶栏“动态”和“收藏”之间加入“稍后再看”按钮,支持弹出式列表菜单"> <span>【所有页面】在顶栏中加入“稍后再看”入口</span><input id="gm_headerButton" type="checkbox"></label> <label class="gm_item" title="在常规播放页面中加入能将视频快速切换添加或移除出“稍后再看”列表的按钮"> <span>【播放页面】加入快速切换“稍后再看”状态的按钮</span><input id="gm_videoButton" type="checkbox"></label> <label class="gm_item" title="是否自动从【www.bilibili.com/medialist/play/watchlater/p*】页面切换至【www.bilibili.com/video/BV*】页面播放"> <span>【播放页面】是否重定向至常规播放页面</span><input id="gm_redirect" type="checkbox"></label> <label class="gm_item" title="在【www.bilibili.com/watchlater/#/list】页面点击时,是否在新标签页打开视频"> <span>【列表页面】是否在新标签页中打开视频</span><input id="gm_openInNew" type="checkbox"></label> <label class="gm_item" title="每次打开【www.bilibili.com/watchlater/#/list】页面时将稍后再看列表数据记录下来,方便误删后查找"> <span>【列表页面】是否开启防误删记录功能</span><input id="gm_watchlaterHistory" type="checkbox"></label> </div> <div class="gm_bottom"> <button id="gm_save">保存</button><button id="gm_cancel">取消</button> </div> </div> <div class="gm_shadow"></div> ` var el_headerButton = el.querySelector('#gm_headerButton') el_headerButton.checked = headerButton var el_videoButton = el.querySelector('#gm_videoButton') el_videoButton.checked = videoButton var el_redirect = el.querySelector('#gm_redirect') el_redirect.checked = redirect var el_openInNew = el.querySelector('#gm_openInNew') el_openInNew.checked = openInNew var el_watchlaterHistory = el.querySelector('#gm_watchlaterHistory') el_watchlaterHistory.checked = watchlaterHistory var save = el.querySelector('#gm_save') save.onclick = () => { headerButton = el_headerButton.checked GM_setValue('gm395456_headerButton', headerButton) videoButton = el_videoButton.checked GM_setValue('gm395456_videoButton', videoButton) redirect = el_redirect.checked GM_setValue('gm395456_redirect', redirect) openInNew = el_openInNew.checked GM_setValue('gm395456_openInNew', openInNew) switchOpenInNew() watchlaterHistory = el_watchlaterHistory.checked GM_setValue('gm395456_watchlaterHistory', watchlaterHistory) el.style.display = 'none' if (initial) { init = configUpdate GM_setValue('gm395456', init) } } var cancel = el.querySelector('#gm_cancel') cancel.disabled = initial cancel.onclick = () => { el.style.display = 'none' el_headerButton.checked = headerButton el_videoButton.checked = videoButton el_redirect.checked = redirect el_openInNew.checked = openInNew el_watchlaterHistory.checked = watchlaterHistory } } } // 打开防误删记录 function openWatchlaterHistory(parent, el, wHistory) { if (el) { el.style.display = '' } else { el = parent.appendChild(document.createElement('div')) el.className = 'gm_history' el.innerHTML = ` <div class="gm_historyPage"> <div class="gm_title">稍后再看防误删记录</div> <div class="gm_comment">仅保留当前页面打开时的稍后再看列表数据,可向下滚动,点击对话框以外的位置退出</div> <div class="gm_content"> ${wHistory.join('<br><br>')} </div> </div> <div class="gm_shadow"></div> ` var el_shadow = el.querySelector('.gm_shadow') el_shadow.onclick = () => { el.style.display = 'none' } } } 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 oVue = original.__vue__ var btn = document.createElement('label') var cb = document.createElement('input') cb.type = 'checkbox' cb.style.verticalAlign = 'middle' cb.style.margin = '0 2px 2px 0' btn.appendChild(cb) var text = document.createElement('span') text.innerText = '稍后再看' btn.className = 'appeal-text' cb.onclick = () => { // 不要附加到 btn 上,否则点击时会执行两次 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.appendChild(text) atr.appendChild(btn) original.parentNode.style.display = 'none' // oVue.added 第一次取到的值总是 false,从页面无法获取到该视频是否已经在稍后再看列表中,需要使用API查询 GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/v2/history/toview/web?jsonp=jsonp`, onload: function(response) { if (response && response.responseText) { try { var json = JSON.parse(response.responseText) var watchList = json.data.list var av = oVue.aid for (var e of watchList) { if (av == e.aid) { oVue.added = true cb.checked = true break } } } catch(e) { console.error(e) } } } }) } // 切换新标签页打开 function switchOpenInNew() { if (/bilibili.com\/watchlater\/.*\/list/.test(location.href)) { var base = null if (openInNew) { base = document.head.appendChild(document.createElement('base')) base.id = 'gm_base' base.target = '_blank' } else { base = document.head.querySelector('base#gm_base') base && base.remove() } } } /** * 在条件满足后执行操作 * * 当条件满足后,如果不存在终止条件,那么直接执行 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)), }) } }) })()