Bilibili 视频截图助手

B站视频截图工具,支持截图按钮、快捷键截图、连拍功能,支持自定义快捷键与连拍间隔,中英菜单切换

当前为 2025-04-15 提交的版本,查看 最新版本

// ==UserScript==
// @name         Bilibili Video Screenshot Helper
// @name:zh-TW   Bilibili 影片截圖助手
// @name:zh-CN   Bilibili 视频截图助手
// @namespace    https://www.tampermonkey.net/
// @version      1.5
// @description  Bilibili Video Screenshot Tool – supports screenshot button, hotkey capture, burst mode, customizable hotkeys and burst intervals, with menu language switch between Chinese and English.
// @description:zh-TW B站影片截圖工具,支持截圖按鈕、快捷鍵截圖、連拍功能,支持自定義快捷鍵與連拍間隔,中英菜單切換
// @description:zh-CN B站视频截图工具,支持截图按钮、快捷键截图、连拍功能,支持自定义快捷键与连拍间隔,中英菜单切换
// @author       ChatGPT
// @match        https://www.bilibili.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // ====== 預設設定 ======
  const DEFAULT_KEY = 'S';
  const DEFAULT_INTERVAL = 1000;
  const MIN_INTERVAL = 100;
  const SETTINGS_LOCK_KEY = 'screenshotHelperSettingsLock';

  // ====== 語言包 ======
  const LANGS = {
    EN: {
      screenshot: 'Screenshot',
      keySetting: key => `Set Screenshot Key (Current: ${key})`,
      intervalSetting: val => `Set Burst Interval (Current: ${val}ms)`,
      langToggle: 'Language: EN',
      keyPrompt: 'Enter new key (A-Z)',
      intervalPrompt: 'Enter new interval in ms (>= 100)',
    },
    ZH: {
      screenshot: '截圖',
      keySetting: key => `設定截圖快捷鍵(目前:${key})`,
      intervalSetting: val => `設定連拍間隔(目前:${val} 毫秒)`,
      langToggle: '語言:ZH',
      keyPrompt: '輸入新快捷鍵(A-Z)',
      intervalPrompt: '輸入新的連拍間隔(最小 100ms)',
    }
  };

  // ====== 目前狀態 ======
  let lang = GM_getValue('lang', 'EN');
  let currentKey = GM_getValue('hotkey', DEFAULT_KEY);
  let interval = GM_getValue('interval', DEFAULT_INTERVAL);
  const t = () => LANGS[lang];

  // ====== 提示鎖定機制(防止彈出兩次) ======
  async function promptWithLock(action) {
    if (localStorage.getItem(SETTINGS_LOCK_KEY) === '1') return;
    localStorage.setItem(SETTINGS_LOCK_KEY, '1');

    // 使用 requestAnimationFrame 等待畫面穩定再解除鎖
    await new Promise(resolve => requestAnimationFrame(resolve));
    localStorage.removeItem(SETTINGS_LOCK_KEY);

    action();
  }

  // ====== 功能設定選單 ======
  GM_registerMenuCommand(t().keySetting(currentKey), () => {
    promptWithLock(() => {
      const input = prompt(t().keyPrompt);
      if (input && /^[a-zA-Z]$/.test(input)) {
        const newKey = input.toUpperCase();
        if (newKey !== currentKey) {
          GM_setValue('hotkey', newKey);
          location.reload();
        }
      }
    });
  });

  GM_registerMenuCommand(t().intervalSetting(interval), () => {
    promptWithLock(() => {
      const input = prompt(t().intervalPrompt);
      const val = parseInt(input);
      if (!isNaN(val) && val >= MIN_INTERVAL && val !== interval) {
        GM_setValue('interval', val);
        location.reload();
      }
    });
  });

  GM_registerMenuCommand(lang === 'EN' ? 'Language: EN' : '語言:ZH', () => {
    promptWithLock(() => {
      GM_setValue('lang', lang === 'EN' ? 'ZH' : 'EN');
      location.reload();
    });
  });

  // ====== 截圖邏輯 ======
  function takeScreenshot() {
    // 只在播放頁面且影片正在播放時截圖
    const video = document.querySelector('video');
    const isVideoPage = /\/video\/(BV\w+)/.test(location.pathname);
    if (!isVideoPage || !video || video.paused) return; // 只有在影片頁且影片播放中才能截圖

    const canvas = document.createElement('canvas');
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

    const pad = n => n.toString().padStart(2, '0');
    const padMs = n => n.toString().padStart(3, '0');
    const bvIdMatch = location.href.match(/\/video\/(BV\w+)/);
    const bvId = bvIdMatch ? bvIdMatch[1] : 'UnknownBV';

    const videoTime = video.currentTime;
    const h = pad(Math.floor(videoTime / 3600));
    const m = pad(Math.floor((videoTime % 3600) / 60));
    const s = pad(Math.floor(videoTime % 60));
    const ms = padMs(Math.floor((videoTime * 1000) % 1000));

    const res = `${canvas.width}x${canvas.height}`;
    const filename = `${h}_${m}_${s}_${ms}_${bvId}_${res}.png`;

    canvas.toBlob(blob => {
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.click();
      URL.revokeObjectURL(url);
    }, 'image/png');
  }

  // ====== 插入按鈕 ======
  function insertScreenshotButton() {
    const qualityBtn = document.querySelector('.bpx-player-ctrl-quality');
    if (!qualityBtn || document.querySelector('.bili-screenshot-btn')) return;

    const btn = document.createElement('div');
    btn.className = 'bpx-player-ctrl-btn bili-screenshot-btn';
    btn.style.display = 'flex';
    btn.style.alignItems = 'center';
    btn.style.justifyContent = 'center';
    btn.style.cursor = 'pointer';
    btn.style.fontSize = '18px';
    btn.style.marginRight = '6px';
    btn.title = t().screenshot;
    btn.innerHTML = '📸';

    btn.addEventListener('click', takeScreenshot);
    qualityBtn.parentNode.insertBefore(btn, qualityBtn);
  }

  // ====== 快捷鍵與連拍 ======
  let holdTimer = null;
  document.addEventListener('keydown', e => {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
    if (e.key.toUpperCase() === currentKey && !holdTimer) {
      takeScreenshot();
      holdTimer = setInterval(takeScreenshot, interval);
    }
  });

  document.addEventListener('keyup', e => {
    if (e.key.toUpperCase() === currentKey && holdTimer) {
      clearInterval(holdTimer);
      holdTimer = null;
    }
  });

  // ====== 監聽 DOM 插入按鈕 ======
  const observer = new MutationObserver(() => {
    insertScreenshotButton();
  });
  observer.observe(document.body, { childList: true, subtree: true });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址