B站/Youtube 倍速快捷键(Z/X/C)+ 宽屏提示(中下方居中)

分段倍速调节,提示显示在播放器底部中间,支持B站宽屏切换,YouTube字幕键屏蔽等功能。

// ==UserScript==
// @name         B站/Youtube 倍速快捷键(Z/X/C)+ 宽屏提示(中下方居中)
// @version      1.1
// @description  分段倍速调节,提示显示在播放器底部中间,支持B站宽屏切换,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);
  let lastRate = GM_getValue("b", 10);

  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: 50%;
    bottom: 45px;
    transform: translateX(-50%);
    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;
    if (real < 4) return 5;
    return 10;
  }

  document.addEventListener("keydown", function (e) {
    if (!isActive) return;
    const key = e.code;
    const isBili = location.hostname.includes("bilibili.com");
    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 === "KeyT") {
      if (isBili) {
        e.preventDefault(); // 仅 B 站拦截
        const wideBtn = document.querySelector(".bpx-player-ctrl-wide");
        if (wideBtn) {
          wideBtn.click();
        }
      }
    }

    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);

  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);
})();