Greasy Fork

Greasy Fork is available in English.

Twitch 截图助手

Twitch 撷取画面工具,支援快捷键、连拍模式、自订快捷键、连拍间隔与中英菜单切换

目前为 2025-04-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         Twitch Screenshot Helper
// @name:zh-TW   Twitch 截圖助手
// @name:zh-CN   Twitch 截图助手
// @namespace    https://yourdomain.com
// @version      1.5
// @description  Twitch screen capture tool with support for hotkeys, burst mode, customizable shortcuts, capture interval, and English/Chinese menu switching.
// @description:zh-TW Twitch 擷取畫面工具,支援快捷鍵、連拍模式、自訂快捷鍵、連拍間隔與中英菜單切換
// @description:zh-CN Twitch 撷取画面工具,支援快捷键、连拍模式、自订快捷键、连拍间隔与中英菜单切换
// @author       ChatGPT
// @match        https://www.twitch.tv/*
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const lang = GM_getValue("lang", "EN"); // EN or ZH
    const screenshotKey = GM_getValue("screenshotKey", "s");
    const intervalTime = parseInt(GM_getValue("shootInterval", "1000"), 10);
    let shootTimer = null;

    const text = {
        EN: {
            btnTooltip: `Screenshot (Shortcut: ${screenshotKey.toUpperCase()})`,
            setKey: `Set Screenshot Key (Current: ${screenshotKey.toUpperCase()})`,
            setInterval: `Set Interval (Current: ${intervalTime}ms)`,
            langSwitch: `language EN`,
            keySuccess: key => `New shortcut key set to: ${key.toUpperCase()}. Please refresh.`,
            keyError: `Please enter a single letter (A-Z).`,
            intervalSuccess: ms => `Interval updated to ${ms}ms. Please refresh.`,
            intervalError: `Please enter a number >= 100`,
        },
        ZH: {
            btnTooltip: `擷取畫面(快捷鍵:${screenshotKey.toUpperCase()})`,
            setKey: `設定快捷鍵(目前為 ${screenshotKey.toUpperCase()})`,
            setInterval: `設定連拍間隔(目前為 ${intervalTime} 毫秒)`,
            langSwitch: `語言 中文`,
            keySuccess: key => `操作成功!新快捷鍵為:${key.toUpperCase()},請重新整理頁面以使設定生效。`,
            keyError: `請輸入單一英文字母(A-Z)!`,
            intervalSuccess: ms => `間隔時間已更新為:${ms}ms,請重新整理頁面以使設定生效。`,
            intervalError: `請輸入 100ms 以上的數字!`,
        }
    }[lang];

    // 取得直播 ID
    function getStreamerId() {
        const match = window.location.pathname.match(/^\/([^\/?#]+)/);
        return match ? match[1] : "unknown";
    }

    // 時間字串:精確到毫秒
    function getTimeString() {
        const now = new Date();
        const pad = n => n.toString().padStart(2, '0');
        const ms = now.getMilliseconds().toString().padStart(3, '0');
        return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}_${pad(now.getMinutes())}_${pad(now.getSeconds())}_${ms}`;
    }

    // 擷取畫面
    function takeScreenshot() {
        const video = document.querySelector('video');
        if (!video || !video.src) {
            console.warn("找不到影片");
            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);

        canvas.toBlob(blob => {
            if (!blob) return;
            const timeStr = getTimeString();
            const streamer = getStreamerId();
            const resolution = `${canvas.width}x${canvas.height}`;
            const a = document.createElement("a");
            a.href = URL.createObjectURL(blob);
            a.download = `${timeStr}_${streamer}_${resolution}.png`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }, "image/png");
    }

    // 開始連拍
    function startContinuousShot() {
        if (shootTimer) return;
        takeScreenshot(); // 立即拍一次
        shootTimer = setInterval(takeScreenshot, intervalTime);
    }

    function stopContinuousShot() {
        clearInterval(shootTimer);
        shootTimer = null;
    }

    // 插入截圖按鈕
    function createIntegratedButton() {
        if (document.querySelector("#screenshot-btn")) return;

        const controls = document.querySelector('.player-controls__right-control-group');
        if (!controls) return;

        const btn = document.createElement("button");
        btn.id = "screenshot-btn";
        btn.innerText = "📸";
        btn.title = text.btnTooltip;
        btn.style.cssText = `
            background: transparent;
            border: none;
            color: white;
            font-size: 20px;
            cursor: pointer;
            margin-left: 10px;
        `;

        btn.addEventListener("mousedown", startContinuousShot);
        btn.addEventListener("mouseup", stopContinuousShot);
        btn.addEventListener("mouseleave", stopContinuousShot);

        controls.appendChild(btn);
    }

    function init() {
        const observer = new MutationObserver(() => {
            createIntegratedButton();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // 判斷是否正在輸入文字(避免快捷鍵誤觸)
    function isTyping() {
        const active = document.activeElement;
        return active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
    }

    // 快捷鍵監聽
    document.addEventListener("keydown", (e) => {
        if (e.key.toLowerCase() === screenshotKey.toLowerCase() && !shootTimer && !isTyping()) {
            e.preventDefault();
            startContinuousShot();
        }
    });

    document.addEventListener("keyup", (e) => {
        if (e.key.toLowerCase() === screenshotKey.toLowerCase() && !isTyping()) {
            e.preventDefault();
            stopContinuousShot();
        }
    });

    // 設定功能表
    GM_registerMenuCommand(text.setKey, () => {
        const input = prompt(lang === "EN" ? "Enter new shortcut key (A-Z)" : "請輸入新的快捷鍵(A-Z)", screenshotKey);
        if (input && /^[a-zA-Z]$/.test(input)) {
            GM_setValue("screenshotKey", input.toLowerCase());
            alert(text.keySuccess(input));
        } else {
            alert(text.keyError);
        }
    });

    GM_registerMenuCommand(text.setInterval, () => {
        const input = prompt(lang === "EN" ? "Enter interval in milliseconds (min: 100)" : "請輸入新的連拍間隔(最小100毫秒)", intervalTime);
        const val = parseInt(input, 10);
        if (!isNaN(val) && val >= 100) {
            GM_setValue("shootInterval", val);
            alert(text.intervalSuccess(val));
        } else {
            alert(text.intervalError);
        }
    });

    // 語言切換,點擊後直接更新值並重新整理
    GM_registerMenuCommand(text.langSwitch, () => {
        GM_setValue("lang", lang === "EN" ? "ZH" : "EN");
        location.reload(); // 自動重新整理頁面
    });

    init();
})();