// ==UserScript==
// @name B站循环助手-精简版
// @namespace bilibili-replayer
// @version 1.17
// @description 稳定可靠的AB点循环工具,适配最新B站页面结构
// @author dms
// @match https://www.bilibili.com/video/BV*
// @match https://www.bilibili.com/bangumi/play/ep*
// @match https://www.bilibili.com/medialist/play/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
// 存储管理
const Storage = {
savePoint: (index, value) => {
try {
GM_setValue(`Point_${index}`, value);
return true;
} catch(e) {
console.error('保存点位失败:', e);
return false;
}
},
getPoint: (index) => {
try {
return GM_getValue(`Point_${index}`, null);
} catch(e) {
console.error('获取点位失败:', e);
return null;
}
}
};
// 工具函数
const Utils = {
createButton(text, className, parent) {
const button = document.createElement('div');
className.split(' ').forEach(c => button.classList.add(c));
button.innerText = text;
parent.appendChild(button);
return button;
},
showNotification(text, title = '提示', timeout = 2000) {
GM_notification({
text,
title,
timeout
});
}
};
class VideoController {
constructor(video) {
this.video = video;
this.points = [0, video.duration-1];
this.pointButtons = [];
this.animationFrameId = null;
this.lastTime = 0;
}
setPoint(index, value) {
if (this.pointButtons[index].classList.contains('active-button')) {
this.points[index] = index ? this.video.duration-1 : 0;
this.pointButtons[index].classList.remove('active-button');
Storage.savePoint(index, null);
} else {
this.points[index] = value;
this.pointButtons[index].classList.add('active-button');
Storage.savePoint(index, this.points[index]);
}
}
startLoop(button) {
if(this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
button.classList.remove('active-button');
button.innerText = '▶循环';
return;
}
button.classList.add('active-button');
button.innerText = '■停止';
const checkLoop = (timestamp) => {
if (timestamp - this.lastTime > 200) {
const A = this.points[0] <= this.points[1] ? this.points[0] : this.points[1];
const B = this.points[0] > this.points[1] ? this.points[0] : this.points[1];
if(this.video.currentTime >= B) {
this.video.currentTime = A;
}
this.lastTime = timestamp;
}
this.animationFrameId = requestAnimationFrame(checkLoop);
};
this.animationFrameId = requestAnimationFrame(checkLoop);
}
}
const createToolbar = async () => {
const video = document.querySelector('#bilibili-player video');
if (!video) return;
const controller = new VideoController(video);
const waitForControl = setInterval(() => {
const controlBar = document.querySelector('.bpx-player-control-bottom');
if (!controlBar) return;
clearInterval(waitForControl);
// 修改现有的播放控制栏布局
const progressBar = controlBar.querySelector('.bpx-player-control-wrap');
const rightControls = controlBar.querySelector('.bpx-player-control-right');
// 确保进度条在顶部
if (progressBar) {
progressBar.style.position = 'absolute';
progressBar.style.top = '0';
progressBar.style.left = '0';
progressBar.style.right = '0';
}
// 创建工具栏容器
const toolbarbox = document.createElement('div');
toolbarbox.style = `
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 10px;
flex-grow: 1;
`;
// 插入工具栏到右侧控制按钮之前
if (rightControls) {
controlBar.insertBefore(toolbarbox, rightControls);
} else {
controlBar.appendChild(toolbarbox);
}
const toolbar = document.createElement('div');
toolbar.id = 'replayer-toolbar';
toolbar.style = `
display: flex;
align-items: center;
height: 100%;
font-size: 12px;
color: #ffffff;
white-space: nowrap;
`;
toolbarbox.appendChild(toolbar);
// 创建按钮
const pointA = Utils.createButton('起点', 'tool-item tool-button', toolbar);
const toA = Utils.createButton('跳A', 'tool-item tool-button', toolbar);
Utils.createButton('|', 'tool-item tool-text', toolbar);
const pointB = Utils.createButton('终点', 'tool-item tool-button', toolbar);
const toB = Utils.createButton('跳B', 'tool-item tool-button', toolbar);
Utils.createButton('|', 'tool-item tool-text', toolbar);
const Start = Utils.createButton('▶循环', 'tool-item tool-button', toolbar);
// 添加按钮样式
const buttons = toolbar.getElementsByClassName('tool-item');
Array.from(buttons).forEach(button => {
button.style.cssText = `
padding: 0 6px;
margin: 0 1px;
border-radius: 2px;
color: #ffffff;
cursor: pointer;
opacity: 0.85;
transition: all 0.2s ease;
`;
if (button.classList.contains('tool-button')) {
button.addEventListener('mouseover', () => {
if (!button.classList.contains('active-button')) {
button.style.opacity = '1';
button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
}
});
button.addEventListener('mouseout', () => {
if (!button.classList.contains('active-button')) {
button.style.opacity = '0.85';
button.style.backgroundColor = '';
}
});
}
});
controller.pointButtons = [pointA, pointB];
// 事件监听
pointA.addEventListener('click', () => {
controller.setPoint(0, video.currentTime);
});
pointB.addEventListener('click', () => {
controller.setPoint(1, video.currentTime);
});
Start.addEventListener('click', () => controller.startLoop(Start));
toA.addEventListener('click', () => { video.currentTime = controller.points[0]; });
toB.addEventListener('click', () => { video.currentTime = controller.points[1]; });
}, 1000);
};
if (document.readyState === 'complete') {
createToolbar();
} else {
window.addEventListener('load', createToolbar);
}
})();