Greasy Fork is available in English.
这一款专为B站用户打造的实用小工具,包含了B站视频的倍速播放(支持快捷键和控制面板设置),以及视频时间查询器(够便捷地计算视频的总时长,并根据不同的倍速计算实际的观看时间)。这款工具除了提供精确的时间统计,还具备窗口拖动、动态样式调整等功能,非常适合在B站学习课程的用户使用;2.x版本将“倍速功能”单独拆分到独立UI和单独热键监听中,并且原来的“时间计算”功能保持不变。
// ==UserScript==
// @name 小叶的b站学习助手(视频时间查询器+视频倍速播放)
// @namespace http://tampermonkey.net/
// @version 2.2.1
// @description 这一款专为B站用户打造的实用小工具,包含了B站视频的倍速播放(支持快捷键和控制面板设置),以及视频时间查询器(够便捷地计算视频的总时长,并根据不同的倍速计算实际的观看时间)。这款工具除了提供精确的时间统计,还具备窗口拖动、动态样式调整等功能,非常适合在B站学习课程的用户使用;2.x版本将“倍速功能”单独拆分到独立UI和单独热键监听中,并且原来的“时间计算”功能保持不变。
// @author 小叶
// @license AGPL License
// @match *://*.bilibili.com/video/*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
'use strict';
// ================== 配置 ==================
const CONFIG = {
UI: {
TRIGGER_ID: 'popup-trigger-container',
CONTAINER_ID: 'time-calculator-container',
RESULT_DIV_ID: 'resultDiv',
DEFAULT_OPACITY: 0.8,
Z_INDEX: 999999,
ICON_URL: 'https://www.bilibili.com/favicon.ico'
},
SPEED_UI: { SHOW_SPEED_UI: true },
STYLE: {
COLORS: {
PRIMARY: '#00A1D6',
SECONDARY: '#40E0D0',
WARNING: '#FF6347',
HOVER: '#008BB5',
TEXT: { PRIMARY: '#333', SECONDARY: '#888' }
},
BORDER_RADIUS: { SMALL: '4px', MEDIUM: '8px', LARGE: '16px' },
TRANSITIONS: { DEFAULT: 'all 0.3s ease' }
},
FEATURES: {
RESULT_DISPLAY_TIME: 15000,
MIN_EPISODE: 1,
MIN_SPEED: 0.5,
SPEED_STEP: 0.1,
DEFAULT_SPEED: 1,
TIME_FORMATS: ['时分秒', '仅小时', '仅分钟', '仅秒']
},
LAYOUT: {
SNAP_PADDING: 20,
CONTAINER_WIDTH: '280px',
TRIGGER_WIDTH: { DEFAULT: '40px', EXPANDED: '80px' }
},
TEXT: {
TRIGGER_TEXT: '小叶计时器',
CLOSE_TEXT: '关闭计时器',
TITLE: '小叶的B站时间查询器',
FOOTER: '小叶计时器',
MESSAGES: {
INVALID_INPUT: '请输入有效的数值。',
MIN_EPISODE: '最小为第1集',
INVALID_RANGE: '输入的集数范围不正确。',
NO_DURATION: '无法获取视频时长,请确保已加载视频列表。',
MAX_EPISODE: '最大为第{count}集'
}
},
CLASSES: { DURATION: 'duration', STATS: 'stats' }
};
// 本地存储键
const STORAGE_KEYS = {
OPACITY: 'containerOpacity',
FACTOR: 'calcFactor', // 仅用于时间计算的倍数
CALC_POS: 'calcPosition', // 时间计算面板位置
SPEED_POS: 'speedUiPosition', // 倍速面板位置
PLAYBACK: 'playbackSpeed' // 实际播放倍速
};
// ============ 状态 ============
let containerOpacity = GM_getValue(STORAGE_KEYS.OPACITY, CONFIG.UI.DEFAULT_OPACITY);
let userSpeedFactor = parseFloat(GM_getValue(STORAGE_KEYS.FACTOR, 1.0)) || 1.0; // 计算倍数(>0)
let isPopupVisible = false;
let resultTimeoutId = null;
// 实际视频倍速(可记忆)
let currentSpeed = parseFloat(GM_getValue(STORAGE_KEYS.PLAYBACK, 1.0)) || 1.0;
let originalSpeed = 1.0;
// ============ 通用:拖拽并记忆位置 ============
function makeElementDraggable(el, storageKey) {
let offsetX = 0, offsetY = 0, isDragging = false;
el.addEventListener('mousedown', (e) => {
const tag = e.target.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select' || e.button !== 0) return;
isDragging = true;
const rect = el.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
el.style.transition = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
el.style.left = `${e.clientX - offsetX}px`;
el.style.top = `${e.clientY - offsetY}px`;
el.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
el.style.transition = CONFIG.STYLE.TRANSITIONS.DEFAULT;
if (storageKey) {
const rect = el.getBoundingClientRect();
GM_setValue(storageKey, JSON.stringify({ left: rect.left, top: rect.top }));
}
});
// 初次挂载时恢复位置
if (storageKey) {
const posRaw = GM_getValue(storageKey, null);
if (posRaw) {
try {
const pos = JSON.parse(posRaw);
if (typeof pos.left === 'number' && typeof pos.top === 'number') {
el.style.left = `${pos.left}px`;
el.style.top = `${pos.top}px`;
el.style.right = 'auto';
}
} catch (_) { }
}
}
}
// ============ 一、时间计算触发器 ============
const createPopupTrigger = () => {
if (!document.querySelector(`.${CONFIG.CLASSES.STATS}`)) {
console.log('没有找到视频元素,触发器不会显示。');
return;
}
const existingTrigger = document.getElementById(CONFIG.UI.TRIGGER_ID);
if (existingTrigger) existingTrigger.remove();
const body = document.body;
const triggerContainer = document.createElement('div');
triggerContainer.id = CONFIG.UI.TRIGGER_ID;
triggerContainer.style.cssText = `
position: fixed;
right: 0;
top: 12%;
transform: translateY(-50%);
z-index: ${CONFIG.UI.Z_INDEX};
text-align: center;
border: 1px solid ${CONFIG.STYLE.COLORS.PRIMARY};
border-radius: ${CONFIG.STYLE.BORDER_RADIUS.MEDIUM};
background-color: rgba(255,255,255,${containerOpacity});
padding: 8px;
width: ${CONFIG.LAYOUT.TRIGGER_WIDTH.DEFAULT};
transition: ${CONFIG.STYLE.TRANSITIONS.DEFAULT};
cursor: pointer;
margin-left: 5px;
`;
const icon = document.createElement('img');
icon.src = CONFIG.UI.ICON_URL;
icon.alt = 'B站图标';
icon.style.cssText = `
width: 24px; height: 24px; display: block; margin: 0 auto;
transition: ${CONFIG.STYLE.TRANSITIONS.DEFAULT};
`;
const textContainer = document.createElement('div');
textContainer.style.cssText = `
font-size: 12px; color: ${CONFIG.STYLE.COLORS.PRIMARY};
margin-top: 4px; white-space: nowrap; overflow: hidden; display: none;
`;
textContainer.innerText = CONFIG.TEXT.TRIGGER_TEXT;
triggerContainer.onmouseenter = () => {
triggerContainer.style.width = CONFIG.LAYOUT.TRIGGER_WIDTH.EXPANDED;
textContainer.style.display = 'block';
};
triggerContainer.onmouseleave = () => {
if (!isPopupVisible) {
triggerContainer.style.width = CONFIG.LAYOUT.TRIGGER_WIDTH.DEFAULT;
textContainer.style.display = 'none';
}
};
triggerContainer.onclick = togglePopup;
triggerContainer.appendChild(icon);
triggerContainer.appendChild(textContainer);
body.appendChild(triggerContainer);
return triggerContainer;
};
const togglePopup = () => {
isPopupVisible = !isPopupVisible;
const triggerContainer = document.getElementById(CONFIG.UI.TRIGGER_ID);
const textContainer = triggerContainer ? triggerContainer.querySelector('div') : null;
if (isPopupVisible) {
createTimeCalcUI();
if (triggerContainer && textContainer) {
triggerContainer.style.width = '80px';
textContainer.style.display = 'block';
textContainer.style.color = '#FF0000';
textContainer.innerText = CONFIG.TEXT.CLOSE_TEXT;
}
} else {
closeTimeCalcUI();
if (triggerContainer && textContainer) {
triggerContainer.style.width = '40px';
textContainer.style.color = CONFIG.STYLE.COLORS.PRIMARY;
textContainer.innerText = CONFIG.TEXT.TRIGGER_TEXT;
}
}
};
// ============ 二、时间计算 UI ============
const createTimeCalcUI = () => {
const existingDiv = document.getElementById(CONFIG.UI.CONTAINER_ID);
if (existingDiv) existingDiv.remove();
const body = document.body;
const container = document.createElement('div');
container.id = CONFIG.UI.CONTAINER_ID;
container.style.cssText = `
padding: 20px;
background-color: rgba(255,255,255,${containerOpacity});
position: fixed;
right: 20px;
top: 20%;
width: 280px; max-width: 90%;
border-radius: 16px;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
border: 1px solid ${CONFIG.STYLE.COLORS.SECONDARY};
z-index: 999999;
text-align: center;
font-size: 14px; color: ${CONFIG.STYLE.COLORS.TEXT.PRIMARY};
`;
makeElementDraggable(container, STORAGE_KEYS.CALC_POS);
const closeButton = document.createElement('button');
closeButton.innerText = '关闭';
closeButton.style.cssText = `
position: absolute; top: 5px; right: 5px;
border: none; background-color: ${CONFIG.STYLE.COLORS.WARNING};
color: #FFF; padding: 5px 10px; cursor: pointer;
border-radius: ${CONFIG.STYLE.BORDER_RADIUS.SMALL};
`;
closeButton.onclick = togglePopup;
container.appendChild(closeButton);
const title = document.createElement('h4');
title.innerText = CONFIG.TEXT.TITLE;
title.style.cssText = `
margin-bottom: 20px; color: ${CONFIG.STYLE.COLORS.PRIMARY};
font-weight: bold; text-align: center;
`;
container.appendChild(title);
// 区间输入
const inputDiv = document.createElement('div');
inputDiv.style.cssText = 'margin-bottom: 15px; display:flex; justify-content:center; align-items:center;';
const label1 = document.createElement('label'); label1.innerText = '从第'; label1.style.cssText = 'margin-right:5px;'; inputDiv.appendChild(label1);
const input1 = document.createElement('input');
input1.type = 'number'; input1.min = '1';
input1.style.cssText = 'border:1px solid deepskyblue;width:50px;text-align:center;margin-right:5px;padding:5px;border-radius:4px;';
inputDiv.appendChild(input1);
const label2 = document.createElement('label'); label2.innerText = '集 到'; label2.style.cssText = 'margin-right:5px;'; inputDiv.appendChild(label2);
const input2 = document.createElement('input');
input2.type = 'number'; input2.min = '1';
input2.style.cssText = 'border:1px solid deepskyblue;width:50px;text-align:center;padding:5px;border-radius:4px;';
inputDiv.appendChild(input2);
container.appendChild(inputDiv);
// 显示格式
const formatDiv = document.createElement('div');
formatDiv.style.cssText = 'margin-bottom: 15px; display:flex; justify-content:center; align-items:center;';
const fLabel = document.createElement('label'); fLabel.innerText = '显示格式:'; fLabel.style.cssText = 'margin-right:5px;'; formatDiv.appendChild(fLabel);
const formatSelect = document.createElement('select');
formatSelect.style.cssText = 'padding:5px;border-radius:4px;border:1px solid deepskyblue;';
CONFIG.FEATURES.TIME_FORMATS.forEach(t => {
const opt = document.createElement('option'); opt.value = t; opt.innerText = t; formatSelect.appendChild(opt);
});
formatDiv.appendChild(formatSelect);
container.appendChild(formatDiv);
// —— 新增:倍数计算(放在透明度上方)——
const factorDiv = document.createElement('div');
factorDiv.style.cssText = 'margin-bottom: 15px; display:flex; justify-content:center; align-items:center; gap:6px; flex-wrap:wrap;';
const factorLabel = document.createElement('label'); factorLabel.innerText = '倍数(仅计算):'; factorDiv.appendChild(factorLabel);
const factorInput = document.createElement('input');
factorInput.type = 'number'; factorInput.min = factorDiv.appendChild(factorInput);
// 一键把“计算倍数”应用为“实际播放倍速”
const applyFactorBtn = document.createElement('button');
applyFactorBtn.innerText = '用此倍数设为播放倍速';
applyFactorBtn.style.cssText = 'border:none;background:#00A1D6;color:#fff;padding:4px 8px;cursor:pointer;border-radius:4px;';
applyFactorBtn.onclick = () => {
const v = parseFloat(factorInput.value);
if (isNaN(v) || v <= 0) { alert('倍数需大于0'); return; }
setAllVideoPlaybackRate(v);
GM_setValue(STORAGE_KEYS.PLAYBACK, v);
};
factorDiv.appendChild(applyFactorBtn);
container.appendChild(factorDiv);
// 透明度
const transparencyDiv = document.createElement('div');
transparencyDiv.style.cssText = 'margin-bottom: 15px; text-align:center;';
const tLabel = document.createElement('label'); tLabel.innerText = '调整透明度:'; transparencyDiv.appendChild(tLabel);
const tSlider = document.createElement('input');
tSlider.type = 'range'; tSlider.min = 0.1; tSlider.max = 1; tSlider.step = 0.1; tSlider.value = containerOpacity;
tSlider.style.cssText = 'margin-left: 10px;';
tSlider.oninput = (e) => {
containerOpacity = parseFloat(e.target.value);
container.style.backgroundColor = `rgba(255,255,255,${containerOpacity})`;
const trig = document.getElementById(CONFIG.UI.TRIGGER_ID);
if (trig) trig.style.backgroundColor = `rgba(255,255,255,${containerOpacity})`;
GM_setValue(STORAGE_KEYS.OPACITY, containerOpacity);
};
transparencyDiv.appendChild(tSlider);
container.appendChild(transparencyDiv);
// 计算按钮
const btn = document.createElement('button');
btn.innerText = '计算时间';
btn.style.cssText = `
width:100%; padding:12px; border:none; background-color:${CONFIG.STYLE.COLORS.PRIMARY};
color:#fff; cursor:pointer; border-radius:8px; font-size:16px; margin-bottom: 12px;
`;
btn.onmouseover = () => { btn.style.backgroundColor = CONFIG.STYLE.COLORS.HOVER; };
btn.onmouseout = () => { btn.style.backgroundColor = CONFIG.STYLE.COLORS.PRIMARY; };
btn.onclick = () => {
calculateTime(formatSelect.value, input1.value, input2.value, factorInput.value);
};
container.appendChild(btn);
// 结果
const resultDiv = document.createElement('div');
resultDiv.id = CONFIG.UI.RESULT_DIV_ID;
resultDiv.style.cssText = 'margin-top: 8px; color:#333; font-weight:bold; text-align:center; white-space:pre-line;';
container.appendChild(resultDiv);
const tip = document.createElement('div');
tip.style.cssText = 'margin-top:6px;color:#888;font-size:12px;';
tip.innerText = '说明:上方“倍数”只影响时间计算;实际播放倍速请用右侧倍速UI或上面的同步按钮。';
container.appendChild(tip);
const footer = document.createElement('div');
footer.innerText = CONFIG.TEXT.FOOTER;
footer.style.cssText = 'margin-top: 10px; color:#888; font-size:12px; text-align:center;';
container.appendChild(footer);
body.appendChild(container);
};
const closeTimeCalcUI = () => {
const existingDiv = document.getElementById(CONFIG.UI.CONTAINER_ID);
if (existingDiv) existingDiv.remove();
};
// ============ 三、时间计算逻辑(支持倍数换算) ============
function calculateTime(format, startStr, endStr, factorStr) {
const allDurations = document.getElementsByClassName(CONFIG.CLASSES.DURATION);
const durations = Array.from(allDurations).filter(el => el.parentElement.className.includes(CONFIG.CLASSES.STATS));
const s = parseInt(startStr, 10);
const e = parseInt(endStr, 10);
const f = parseFloat(factorStr);
if (isNaN(s) || isNaN(e)) { return updateResult(CONFIG.TEXT.MESSAGES.INVALID_INPUT); }
if (s < CONFIG.FEATURES.MIN_EPISODE) { return updateResult(CONFIG.TEXT.MESSAGES.MIN_EPISODE); }
if (e < s) { return updateResult(CONFIG.TEXT.MESSAGES.INVALID_RANGE); }
if (durations.length === 0) { return updateResult(CONFIG.TEXT.MESSAGES.NO_DURATION); }
if (e > durations.length) {
const msg = CONFIG.TEXT.MESSAGES.MAX_EPISODE.replace('{count}', durations.length);
return updateResult(msg);
}
if (isNaN(f) || f <= 0) { alert('倍数需大于0'); return; }
GM_setValue(STORAGE_KEYS.FACTOR, f); // 记忆倍数
// 求原总秒数
let totalSeconds = 0;
for (let i = s - 1; i < e; i++) {
const t = durations[i].innerText.trim();
const parts = t.split(':').map(Number);
let sec = parts.pop() || 0;
let min = parts.pop() || 0;
let hr = parts.pop() || 0;
totalSeconds += hr * 3600 + min * 60 + sec;
}
// 原始时长
const origin = formatTime(totalSeconds, format);
// 按倍数折算后的“实际观看时长”
const adjustedSeconds = totalSeconds / f;
const adjusted = formatTime(adjustedSeconds, format);
// 同时显示两行(更直观)
updateResult(`原总时长:${origin}\n按倍数×${f} 后:${adjusted}`);
}
function formatTime(totalSeconds, format) {
const s = Math.floor(totalSeconds);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = Math.floor(s % 60);
switch (format) {
case '时分秒': return `${h}时${m}分${sec}秒`;
case '仅小时': return `${(s / 3600).toFixed(2)} 小时`;
case '仅分钟': return `${(s / 60).toFixed(2)} 分钟`;
case '仅秒': return `${s} 秒`;
default: return `${h}时${m}分${sec}秒`;
}
}
function updateResult(text) {
const resultDiv = document.getElementById(CONFIG.UI.RESULT_DIV_ID);
if (!resultDiv) return;
resultDiv.innerText = text;
if (resultTimeoutId) clearTimeout(resultTimeoutId);
resultTimeoutId = setTimeout(() => {
if (resultDiv) resultDiv.innerText = '';
resultTimeoutId = null;
}, CONFIG.FEATURES.RESULT_DISPLAY_TIME);
}
// ============ 四、实际视频倍速(保留原功能 + 记忆 + UI + 快捷键) ============
function setAllVideoPlaybackRate(speed) {
if (!speed || isNaN(speed) || speed <= 0) return;
currentSpeed = speed;
const videos = document.querySelectorAll('video');
videos.forEach(v => { try { v.playbackRate = speed; } catch (e) {} });
displaySpeedOnVideo(speed);
updateSpeedDisplay(speed);
}
function displaySpeedOnVideo(speed) {
const videoElement = document.querySelector('video');
if (!videoElement) return;
let existing = videoElement.parentElement.querySelector('.speed-display');
if (existing) existing.remove();
const speedDisplay = document.createElement('div');
speedDisplay.innerText = `倍速:${speed}`;
speedDisplay.style.position = 'absolute';
speedDisplay.style.top = '10px';
speedDisplay.style.left = '10px';
speedDisplay.style.padding = '5px';
speedDisplay.style.backgroundColor = 'rgba(0,0,0,0.7)';
speedDisplay.style.color = '#fff';
speedDisplay.style.fontSize = '16px';
speedDisplay.style.fontWeight = 'bold';
speedDisplay.style.borderRadius = '5px';
speedDisplay.className = 'speed-display';
videoElement.parentElement.appendChild(speedDisplay);
setTimeout(() => { speedDisplay.remove(); }, 3000);
}
function updateSpeedDisplay(speed) {
const label = document.getElementById('current-speed-label');
if (label) label.innerText = `当前倍速:${speed}x`;
GM_setValue(STORAGE_KEYS.PLAYBACK, speed);
}
// 右侧“倍速”触发器 + 面板
const SPEED_TRIGGER_ID = 'speed-trigger-container';
let isSpeedPopupVisible = false;
function createSpeedTrigger() {
if (!CONFIG.SPEED_UI.SHOW_SPEED_UI) return;
const ex = document.getElementById(SPEED_TRIGGER_ID);
if (ex) ex.remove();
const body = document.body;
const trigger = document.createElement('div');
trigger.id = SPEED_TRIGGER_ID;
trigger.style.cssText = `
position: fixed; right: 0; top: 25%; transform: translateY(-50%);
z-index: 999999; text-align: center; border:1px solid #FF8C00;
border-radius: 8px; background-color: rgba(255,255,255,${containerOpacity});
padding: 8px; width: 40px; transition: all .3s ease; cursor: pointer;
`;
const icon = document.createElement('div');
icon.innerText = '倍速';
icon.style.cssText = `font-size:14px;color:#FF8C00;text-align:center;user-select:none;`;
trigger.appendChild(icon);
trigger.addEventListener('click', toggleSpeedPopup);
trigger.onmouseenter = () => { trigger.style.width = '80px'; };
trigger.onmouseleave = () => { if (!isSpeedPopupVisible) trigger.style.width = '40px'; };
body.appendChild(trigger);
}
function toggleSpeedPopup() {
isSpeedPopupVisible = !isSpeedPopupVisible;
const trig = document.getElementById(SPEED_TRIGGER_ID);
if (isSpeedPopupVisible) {
createSpeedUI();
if (trig) trig.style.width = '80px';
} else {
closeSpeedUI();
if (trig) trig.style.width = '40px';
}
}
function createSpeedUI() {
if (document.getElementById('speed-ui-container')) return;
const body = document.body;
const container = document.createElement('div');
container.id = 'speed-ui-container';
container.style.cssText = `
padding: 10px; background-color: rgba(255,255,255,${containerOpacity});
position: fixed; right: 80px; top: 25%; width: 220px; max-width: 60%;
border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);
border: 1px solid #FF8C00; z-index: 999999; text-align: center;
font-size: 14px; color:#444;
`;
makeElementDraggable(container, STORAGE_KEYS.SPEED_POS);
const closeBtn = document.createElement('button');
closeBtn.innerText = '关闭';
closeBtn.style.cssText = `
position:absolute; top:5px; right:5px; border:none; background:#f90;
color:#fff; padding:2px 6px; cursor:pointer; border-radius:4px;
`;
closeBtn.onclick = toggleSpeedPopup;
container.appendChild(closeBtn);
const title = document.createElement('h4');
title.innerText = '视频倍速设置';
title.style.cssText = 'margin-bottom:8px;color:#FF8C00;font-weight:bold;';
container.appendChild(title);
const label = document.createElement('label');
label.innerText = '请输入倍速:';
label.style.cssText = 'margin-right:5px;';
container.appendChild(label);
const speedInput = document.createElement('input');
speedInput.type = 'number';
speedInput.value = currentSpeed;
speedInput.min = '0.1'; speedInput.step = '0.1';
speedInput.style.cssText = 'width:70px;text-align:center;border-radius:4px;border:1px solid #ccc;margin-bottom:10px;';
container.appendChild(speedInput);
const currentSpeedLabel = document.createElement('div');
currentSpeedLabel.id = 'current-speed-label';
currentSpeedLabel.style.cssText = 'margin-top:8px;color:#555;font-size:14px;';
currentSpeedLabel.innerText = `当前倍速:${currentSpeed}x`;
container.appendChild(currentSpeedLabel);
const setBtn = document.createElement('button');
setBtn.innerText = '应用';
setBtn.style.cssText = 'margin-left:8px;background:#00A1D6;color:#fff;border:none;padding:4px 8px;cursor:pointer;border-radius:4px;';
setBtn.onclick = () => {
const val = parseFloat(speedInput.value) || 1;
if (val <= 0) { alert('非法倍速值!'); return; }
setAllVideoPlaybackRate(val);
GM_setValue(STORAGE_KEYS.PLAYBACK, val);
};
container.appendChild(setBtn);
const tips = document.createElement('div');
tips.style.cssText = 'margin-top:10px;color:#888;font-size:12px;white-space:pre-line;';
tips.innerText = '快捷键:\n1-4:设为1x/2x/3x/4x\nC/X:加速/减速0.1\nShift+↑/↓:加速/减速0.1\nShift+0:复位1.0x\nZ:在当前倍速与原始1.0x之间切换';
container.appendChild(tips);
body.appendChild(container);
}
function closeSpeedUI() {
const el = document.getElementById('speed-ui-container');
if (el) el.remove();
}
// 键盘快捷键(保持原逻辑)
window.addEventListener('keydown', (e) => {
const tag = document.activeElement.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea') return;
if (e.shiftKey && !e.ctrlKey && !e.altKey) {
if (e.key === 'ArrowUp') {
currentSpeed = parseFloat((currentSpeed + 0.1).toFixed(2));
setAllVideoPlaybackRate(currentSpeed);
} else if (e.key === 'ArrowDown') {
let ns = parseFloat((currentSpeed - 0.1).toFixed(2));
if (ns < 0.1) ns = 0.1;
currentSpeed = ns;
setAllVideoPlaybackRate(currentSpeed);
} else if (e.key === '0') {
currentSpeed = 1.0;
setAllVideoPlaybackRate(1.0);
}
}
if (e.shiftKey && !e.ctrlKey && !e.altKey && (e.key === 'u' || e.key === 'U')) {//显示关闭UI
toggleSpeedPopup();
togglePopup();
}
if (e.key >= '1' && e.key <= '4') {
const map = { '1': 1.0, '2': 2.0, '3': 3.0, '4': 4.0 };
setAllVideoPlaybackRate(map[e.key]);
}
if (e.key === 'c' || e.key === 'C') {
currentSpeed = parseFloat((currentSpeed + 0.1).toFixed(2));
setAllVideoPlaybackRate(currentSpeed);
}
if (e.key === 'x' || e.key === 'X') {
currentSpeed = parseFloat((currentSpeed - 0.1).toFixed(2));
if (currentSpeed < 0.1) currentSpeed = 0.1;
setAllVideoPlaybackRate(currentSpeed);
}
if (e.key === 'z' || e.key === 'Z') {
if (currentSpeed === 1.0) {
setAllVideoPlaybackRate(originalSpeed);
} else {
originalSpeed = currentSpeed;
setAllVideoPlaybackRate(1.0);
}
}
});
// ============ 初始化 ============
createPopupTrigger();
createSpeedTrigger();
// 页面加载时自动应用上次的“实际播放倍速”
if (currentSpeed && currentSpeed > 0 && currentSpeed !== 1.0) {
setAllVideoPlaybackRate(currentSpeed);
}
})();