Greasy Fork

Greasy Fork is available in English.

小叶的b站学习助手(视频时间查询器+视频倍速播放)

这一款专为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);
  }

})();