B站/Youtube 倍速快捷键(Z/X/C)

Z原速/X减速/C加速(分段步进,支持0.2x),播放器左下角提示 + 屏蔽YouTube字幕快捷键

目前為 2025-05-30 提交的版本,檢視 最新版本

// ==UserScript==
// @name         B站/Youtube 倍速快捷键(Z/X/C)
// @version      1.0
// @description  Z原速/X减速/C加速(分段步进,支持0.2x),播放器左下角提示 + 屏蔽YouTube字幕快捷键
// @author       重音(support by GPT)
// @match        https://www.bilibili.com/*
// @match        https://www.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      GPL
// @namespace https://greasyfork.org/users/1476609
// ==/UserScript==

(function () {
  "use strict";

  let isActive = 1;
  let video = null;
  let currentRate = GM_getValue("a", 10); // 初始播放速度(单位:10 表示 1.0x)
  let lastRate = GM_getValue("b", 10);    // 上次非 1.0 的倍速

  function updateVideoElement() {
    video = document.querySelector("video") || document.querySelector("bwp-video");
  }

  function setupFocusHandlers() {
    const inputs = [
      document.querySelector(".reply-box-textarea"),
      document.querySelector(".bpx-player-dm-input"),
      document.querySelector(".nav-search-input")
    ];
    inputs.forEach(input => {
      if (input) {
        input.addEventListener("focus", () => isActive = 0);
        input.addEventListener("blur", () => isActive = 1);
      }
    });
  }

  window.addEventListener("load", () => {
    updateVideoElement();
    setupFocusHandlers();
  });

  new MutationObserver(() => {
    updateVideoElement();
    setupFocusHandlers();
  }).observe(document.body, { childList: true, subtree: true });

  setInterval(() => {
    if (video) {
      video.playbackRate = currentRate / 10;
    }
  }, 600);

  // 创建气泡提示
  const tip = document.createElement("div");
  tip.style.cssText = `
    position: absolute;
    left: 10px;
    bottom: 10px;
    background: rgba(0, 0, 0, 0.75);
    color: #fff;
    padding: 6px 12px;
    border-radius: 8px;
    font-size: 14px;
    z-index: 99999;
    opacity: 0;
    transition: opacity 0.3s;
    pointer-events: none;
  `;

  function attachTipToPlayer() {
    const player = document.querySelector(".bpx-player-container") || document.querySelector(".html5-video-player") || document.querySelector("video")?.parentElement;
    if (player && player.appendChild && !tip.parentElement) {
      if (getComputedStyle(player).position === "static") {
        player.style.position = "relative";
      }
      player.appendChild(tip);
    }
  }
  attachTipToPlayer();
  setInterval(attachTipToPlayer, 1000);

  let tipTimer = null;
  function showTip(text) {
    tip.textContent = text;
    tip.style.opacity = "1";
    clearTimeout(tipTimer);
    tipTimer = setTimeout(() => {
      tip.style.opacity = "0";
    }, 1200);
  }

  // 获取当前播放速率下应使用的步进值
  function getStep(rate10) {
    const real = rate10 / 10;
    if (real < 2) return 2;   // 0.2x~2.0x:±0.2
    if (real < 4) return 5;   // 2.0x~4.0x:±0.5
    return 10;                // 4.0x~8.0x:±1.0
  }

  // 快捷键监听(捕获阶段)
  document.addEventListener("keydown", function (e) {
    if (!isActive) return;
    const key = e.code;
    const isYoutube = location.hostname.includes("youtube.com");

    // 屏蔽 YouTube C 字幕快捷键
    if (isYoutube && key === "KeyC") {
      e.stopImmediatePropagation();
      e.preventDefault();
    }

    if (!video) return;

    currentRate = Math.round(10 * video.playbackRate);
    const step = getStep(currentRate);
    let changed = false;

    if (key === "KeyX") {
      e.preventDefault();
      currentRate -= step;
      changed = true;
    } else if (key === "KeyC") {
      e.preventDefault();
      currentRate += step;
      changed = true;
    } else if (key === "KeyZ") {
      e.preventDefault();
      currentRate = video.playbackRate === 1.0 ? lastRate : 10;
      changed = true;
    } else if (key === "KeyF") {
      if (document.fullscreenElement) {
        document.exitFullscreen();
      } else {
        document.documentElement.requestFullscreen();
      }
    }

    // 限制播放速率范围(最小 0.2x,最大 8.0x)
    currentRate = Math.max(2, Math.min(currentRate, 80));

    if (changed) {
      GM_setValue("a", currentRate);
      if (key !== "KeyZ") {
        lastRate = currentRate;
        GM_setValue("b", lastRate);
      }
      video.playbackRate = currentRate / 10;
      showTip(`播放速度:${(currentRate / 10).toFixed(1)}x`);
    }
  }, true);

  // 同步 B站倍速面板显示的值
  setInterval(() => {
    const rateDisplay = document.querySelector(".bpx-player-ctrl-playbackrate-result");
    if (rateDisplay) {
      const val = parseFloat(rateDisplay.textContent.replace("x", ""));
      if (!isNaN(val)) {
        currentRate = Math.round(val * 10);
        GM_setValue("a", currentRate);
      }
    }
  }, 2000);
})();