- // ==UserScript==
- // @name bili自定义播放速率
- // @namespace Violentmonkey Scripts
- // @match https://www.bilibili.com/*
- // @grant GM_addStyle
- // @version 1.1
- // @author vurses
- // @license GPL
- // @description 哔哩哔哩自定义视频播放速度,shift1~9快捷键更改速度。插入样式比较违和,只在普通视频下才有自定义按钮,番剧等页面只能通过快捷键更改速度。
- // ==/UserScript==
- (async () => {
- // 在video页面下能使用额外的一些按钮,在其它页面只能使用shift1~9快捷键
- // 每次currentEle必须由筛选的下标获取到dom元素,无需本地存储,知道了默认播放速度就知道了数据列表下标,就知道了选中的元素(但此dom元素也不一定存在),不必要存储
- // 速度列表listproxy,当前速度信息对象objproxy,视频video当前速度pbRList
- // 切换自定义的倍速按钮触发objproxy代理=>更改样式同时触发video的ratechange事件=>
- // 用户切换速度的方式有多种,所以ratechange改变时也需要更改一次样式,该元素通过listproxy过滤出第一个对应速度index可知
- // 点击新增按钮=>更改listproxy触发代理=>重新渲染自定义的所有倍速按钮
-
- // 只有/video下的视频dom结构会优先加载
- let video = document.querySelector("video");
- const waitForVideo = new Promise((resolve, reject) => {
- let checkInterval;
- let timeout;
- function checkVideoExists() {
- video = document.querySelector("video");
- if (video) {
- clearInterval(checkInterval);
- clearTimeout(timeout);
- resolve("video加载成功");
- }
- }
- function stopChecking() {
- clearInterval(checkInterval);
- reject("video获取超时..."); // 8秒内不存在,reject Promise
- }
- // 每100ms检查一次a是否存在
- checkInterval = setInterval(checkVideoExists, 100);
-
- // 8秒后停止检查
- timeout = setTimeout(stopChecking, 8000);
- });
- // 8秒内未加载出video抛出异常中断代码执行
- await waitForVideo;
-
- // 保证有video元素的情况下快捷键能使用
- // shift+1~9快捷键修改速度
- document.addEventListener("keydown", function (event) {
- // 检查是否按下了Shift键
- if (event.shiftKey && event.keyCode >= 51 && event.keyCode <= 57) {
- video.playbackRate = event.which - 48;
- }
- });
- // 以评论区dom的加载为标志
- const commentContainer = document.querySelector("#comment");
-
- // 用定时器也行
- // 创建观察者实例,bili的dom结构有部分异步加载,避免页面出现问题脚本的所有操作得在dom完全加载之后才执行
- // 只要能标志dom结构完全加载,选择观察哪个dom元素的容器都无所谓
- const domLoadedSignalObserver = new MutationObserver(
- (mutationsList, observer) => {
- // 监听回调
- scriptCallback();
- // 移除监听
- domLoadedSignalObserver.disconnect();
- }
- );
- // 在/video下才加载样式和脚本,避免出错。其它页面与/video不同,插入一些样式比较违和
- if (window.location.href.includes("video")) {
- domLoadedSignalObserver.observe(commentContainer, {
- //监听异步加载dom时,childList变更以触发observe
- childList: true,
- });
- } else {
- return;
- }
- // 样式
- const link = document.createElement("link");
- link.rel = "stylesheet";
- // bulma.css用require引入会报错
- // link.href =
- // "https://cdn.bootcdn.net/ajax/libs/bulma/1.0.1/css/bulma.min.css";
- // 无语,bulma默认暗黑就算了,还有样式污染,还得一个个改类名前缀
- // link.href =
- // "https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/versions/bulma-prefixed.min.css";
- // jsdelivr太慢了
- link.href =
- "https://unpkg.com/bulma@1.0.0/css/versions/bulma-prefixed.min.css";
- document.head.appendChild(link);
- // bulma带来的样式污染(ˉ▽ˉ;)...如果样式污染问题太严重得重构
- const styleConflictPatch = document.createElement("style");
- styleConflictPatch.rel = "text/css";
- // 高能进度条svg,一键三连弹出框,视频底部控件样式
- styleConflictPatch.innerHTML = `div.bpx-player-pbp.pin svg{
- width:100% !important;
- height:100% !important;
- }
- .bili-guide-all *{
- box-sizing:content-box !important;
- }
- .bpx-player-control-bottom > div {
- box-sizing:content-box !important;
- }
- `;
- document.head.appendChild(styleConflictPatch);
- // bulma样式默认黑暗模式很蛋疼
- const script = document.createElement("script");
- script.innerHTML = `document.documentElement.setAttribute("data-bulma-theme", "light")`;
- document.head.appendChild(script);
- // 一些额外的样式
- (() => {
- const css = `.script-box {
- width: 100%;
- display: flex;
- }
- .tag-box a:nth-of-type(1):hover {
- background-color: skyblue;
- }
- .tag-box a:nth-of-type(2):hover {
- background-color: rgb(243, 142, 140);
- }
- .new-tag-box {
- width: 70px;
- }
- .bulma-is-blue{
- color: white;
- background-color: lightskyblue;
- }
- `;
- GM_addStyle(css);
- })();
-
- // 将脚本所有操作放一个函数里供observer回调
- const scriptCallback = () => {
- /*********** 函数定义 ***************/
- // 给每一个倍速按钮设置的监听器,用css也能实现,但有一些样式问题还是选择用js
- const setListeners = () => {
- const mouseenterListeners = new WeakMap();
- const mouseleaveListeners = new WeakMap();
- const mouseenterCallback = function (e) {
- const deleteTag = document.createElement("a");
- deleteTag.className = "bulma-tag bulma-is-delete";
- // <div class="tags has-addons">
- this.children[0].append(deleteTag);
- };
- const mouseleaveCallback = function (e) {
- // <div class="tags has-addons">
- this.children[0].lastElementChild.remove();
- };
- Array.from(document.querySelectorAll(".bulma-control")).forEach(
- (element) => {
- // weakmap保存监听函数防止内存泄漏
- mouseenterListeners.set(element, mouseenterCallback);
- mouseleaveListeners.set(element, mouseleaveCallback);
- element.addEventListener(
- "mouseenter",
- mouseenterListeners.get(element)
- );
- element.addEventListener(
- "mouseleave",
- mouseleaveListeners.get(element)
- );
- }
- );
- };
- // 播放速度数组的渲染
- const tagRender = (list) => {
- return list
- .map((value, index) => {
- return `<div class="bulma-control" data-index=${index} style="width:70px">
- <div class="bulma-tags bulma-has-addons">
- <a class="bulma-tag" style="width:40px">${value.toFixed(
- 2
- )}x</a>
- </div>
- </div>`;
- })
- .join("");
- };
- // 计算当前速度对应下标和元素对象
- const computeRateObj = (pbRList, curpbRate) => {
- const index = pbRList.findIndex((value, index) => {
- return value === curpbRate;
- });
- const element =
- index === -1
- ? null
- : document.querySelector(`.bulma-field div:nth-child(${index + 1})`)
- .children[0].children[0];
- return { index, element };
- };
- /*********** 用户界面构建 ***************/
- // 用户操作的容器
- const scriptBox = document.createElement("div");
- scriptBox.className = "script-box pb-1";
- document.querySelector("#viewbox_report").style.height = "auto";
- document.querySelector("#viewbox_report").append(scriptBox);
- document.querySelector(".script-box").innerHTML = `
- <div class="tag-box">
- <div class="bulma-field bulma-is-grouped bulma-is-grouped-multiline">
- </div>
- </div>
- <div class="new-tag-box">
- <button class="bulma-button bulma-is-small" style="height: 21px">+ Rate</button>
- <input class="bulma-input bulma-is-small bulma-is-info" type="number" style="width: 60px;height: 21px; display: none;" />
- </div>`;
-
- // 播放速度列表数组
- const playbackRateList = JSON.parse(
- localStorage.getItem("PLAYBACK_RATE_LIST_GREASYFORK") ||
- "[0.5, 0.75, 1, 1.25, 1.5, 2, 3]"
- );
-
- // 首次渲染
- document.querySelector(".tag-box .bulma-field").innerHTML =
- tagRender(playbackRateList);
-
- // 为每个按钮添加监听器控制样式
- setListeners();
-
- /*********** 响应式,劫持数据操作驱动页面变化 ***************/
- // 默认播放速度
- const currentPlaybackRate = JSON.parse(
- localStorage.getItem("CURRENT_PLAYBACK_RATE_GREASYFORK") || "1"
- );
- // 将视频速度修改为默认速度
- video.playbackRate = currentPlaybackRate;
-
- // 记录默认播放速度(当前播放速度)和当前被选tag的对象
- const { index: currentRateIndex, element: currentRateEle } = computeRateObj(
- playbackRateList,
- currentPlaybackRate
- );
- // 高亮倍速按钮
- if (currentRateEle) currentRateEle.classList.add("bulma-is-blue");
-
- const playbackRateObj = {
- currentRate: currentPlaybackRate,
- currentRateElement: currentRateEle,
- };
-
- // 代理播放速度数组的set操作
- const playbackRateListProxy = new Proxy(playbackRateList, {
- set: (target, key, value) => {
- target[key] = value;
- localStorage.setItem(
- "PLAYBACK_RATE_LIST_GREASYFORK",
- JSON.stringify(target)
- );
- tagRender(target);
- /* 性能可优化 */
- // 重复渲染
- document.querySelector(".bulma-field").innerHTML =
- tagRender(playbackRateList);
- const { element } = computeRateObj(
- playbackRateList,
- playbackRateObjProxy.currentRate
- );
- // 移除元素高亮
- playbackRateObjProxy.currentRateElement = element;
- // 添加元素高亮
- if (element) element.classList.add("bulma-is-blue");
- // 重复添加监听器
- setListeners();
- return Reflect.set(target, key, value);
- },
- });
- // 代理持久化播放速度对象属性的set操作
- const playbackRateObjProxy = new Proxy(playbackRateObj, {
- set: (target, key, value) => {
- // 需要存在符合条件的tag再移除样式
- key === "currentRateElement" &&
- target[key] &&
- target[key] !== value &&
- target[key].classList.remove("bulma-is-blue");
- key === "currentRate" &&
- (video.playbackRate = value) &&
- localStorage.setItem("CURRENT_PLAYBACK_RATE_GREASYFORK", value);
- target[key] = value;
- return Reflect.set(target, key, value);
- },
- });
-
- // 速度变化监听
- video.addEventListener("ratechange", function () {
- // 默认速度持久化
- playbackRateObjProxy.currentRate = video.playbackRate;
- const { element } = computeRateObj(
- playbackRateList,
- playbackRateObjProxy.currentRate
- );
- // 移除元素高亮
- playbackRateObjProxy.currentRateElement = element;
- // 添加元素高亮
- if (element) element.classList.add("bulma-is-blue");
- });
-
- // 根元素事件委托,处理各种事件
- document
- .querySelector(".script-box")
- .addEventListener("click", function (e) {
- switch (e.target.tagName) {
- case "BUTTON":
- const _this = this;
- const newTagBox = _this.children[1];
- const button = newTagBox.children[0];
- const input = newTagBox.children[1];
- button.style.display = "none";
- // 只创建一次回调函数, [0]button || [1]input
- if (!input.onblur) {
- input.onblur = function () {
- button.style.display = "";
- input.style.display = "none";
- // 数字过滤
- this.value &&
- this.value >= 0 &&
- (this.value >= 16
- ? playbackRateListProxy.push(16)
- : playbackRateListProxy.push(Number(this.value)));
- this.value = "";
- button.focus();
- };
- input.onkeydown = function (e) {
- // 两个Enter键
- if (e.keyCode === 108 || e.keyCode === 13) {
- this.onblur();
- }
- // Esc键
- if (e.keyCode === 27) {
- this.value = "";
- this.blur();
- button.blur();
- }
- };
- }
- // 显示并聚焦input
- input.style.display = "";
- input.focus();
- break;
- case "A":
- // type:string
- const index =
- e.target.parentElement.parentElement.getAttribute("data-index");
- // 数据变更驱动dom更新
- if (Array.from(e.target.classList).includes("bulma-is-delete")) {
- // index of tags
- playbackRateListProxy.splice(index, 1);
- } else {
- playbackRateObjProxy.currentRate = playbackRateListProxy[index];
- e.target.classList.add("bulma-is-blue");
- // tags样式互斥,每次存入当前选中的tag方便下次移除样式
- playbackRateObjProxy.currentRateElement = e.target;
- }
- break;
- }
- });
- // 当前页面切换视频引起video属性变换
- const videoChangeObserver = new MutationObserver(
- (mutationsList, observer) => {
- // 避免视频速度被初始化
- video.playbackRate = playbackRateObjProxy.currentRate;
- }
- );
- videoChangeObserver.observe(video, {
- attributes: true,
- });
- };
- })();