// ==UserScript== // @name Twitch Screenshot Helper // @name:zh-TW Twitch 截圖助手 // @name:zh-CN Twitch 截图助手 // @namespace http://tampermonkey.net/ // @version 2.1 // @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 // @run-at document-idle // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 取得語言、快捷鍵、連拍間隔等設定 const lang = GM_getValue("lang", "en").toLowerCase(); // 語言(en/zh-tw) const screenshotKey = GM_getValue("screenshotKey", "s"); // 快捷鍵 const intervalTime = parseInt(GM_getValue("shootInterval", "1000"), 10); // 連拍間隔(ms) let shootTimer = null; // 連拍定時器 let debounceTimeout = null; // 防抖用於按鈕插入 // 多語系文字 const textMap = { en: { btnTooltip: `Screenshot (Shortcut: ${screenshotKey.toUpperCase()})`, setKey: `Set Screenshot Key (Current: ${screenshotKey.toUpperCase()})`, setInterval: `Set Interval (Current: ${intervalTime}ms)`, langSwitch: `language EN`, keyError: `Please enter a single letter (A-Z).`, intervalError: `Please enter a number >= 100`, }, "zh-tw": { btnTooltip: `擷取畫面(快捷鍵:${screenshotKey.toUpperCase()})`, setKey: `設定快捷鍵(目前為 ${screenshotKey.toUpperCase()})`, setInterval: `設定連拍間隔(目前為 ${intervalTime} 毫秒)`, langSwitch: `語言 中文`, keyError: `請輸入單一英文字母(A-Z)!`, intervalError: `請輸入 100ms 以上的數字!`, } }; const text = textMap[lang] || textMap["en"]; // 取得目前直播主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.readyState < 2) 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 a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `${getTimeString()}_${getStreamerId()}_${canvas.width}x${canvas.height}.png`; a.style.display = "none"; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(a.href); }, 100); }, "image/png"); } // 開始連拍 function startContinuousShot() { if (shootTimer) return; takeScreenshot(); shootTimer = setInterval(takeScreenshot, intervalTime); } // 停止連拍 function stopContinuousShot() { clearInterval(shootTimer); shootTimer = null; } // 插入截圖按鈕到 Twitch 控制列 function createIntegratedButton() { if (document.querySelector("#screenshot-btn")) return; // 已存在就不重複插入 // 嘗試多個常見控制列選擇器 const controls = document.querySelector('.player-controls__right-control-group') || document.querySelector('[data-a-target="player-controls-right-group"]'); if (!controls) { // 控制列還沒出現,稍後重試 setTimeout(createIntegratedButton, 1000); return; } // 建立按鈕 const btn = document.createElement("button"); btn.id = "screenshot-btn"; btn.innerHTML = "📸"; btn.title = text.btnTooltip; Object.assign(btn.style, { background: 'transparent', border: 'none', color: 'white', fontSize: '20px', cursor: 'pointer', marginLeft: '8px', display: 'flex', alignItems: 'center', order: 9999, zIndex: '2147483647' }); // 綁定滑鼠事件(支援連拍) btn.addEventListener('mousedown', startContinuousShot, { capture: true }); btn.addEventListener('mouseup', stopContinuousShot, { capture: true }); btn.addEventListener('mouseleave', stopContinuousShot, { capture: true }); // 插入到控制列最右側 try { const referenceNode = controls.querySelector('[data-a-target="player-settings-button"]'); if (referenceNode) { controls.insertBefore(btn, referenceNode); } else { controls.appendChild(btn); } } catch (e) { controls.appendChild(btn); } } // 防抖:避免頻繁重複插入按鈕 function createIntegratedButtonDebounced() { if (debounceTimeout) clearTimeout(debounceTimeout); debounceTimeout = setTimeout(createIntegratedButton, 500); } // 初始化主流程 function init() { createIntegratedButton(); // 監控整個 body,偵測 DOM 變動時自動補回按鈕 const observer = new MutationObserver(createIntegratedButtonDebounced); observer.observe(document.body, { childList: true, subtree: true }); // 每5秒定時檢查按鈕是否存在 setInterval(() => { if (!document.querySelector("#screenshot-btn")) { createIntegratedButton(); } }, 5000); } // 判斷目前是否在輸入框內輸入 function isTyping() { const active = document.activeElement; return active && ['INPUT', 'TEXTAREA'].includes(active.tagName) || active.isContentEditable; } // 快捷鍵事件:支援單鍵連拍 document.addEventListener("keydown", e => { if ( e.key.toLowerCase() === screenshotKey.toLowerCase() && !shootTimer && !isTyping() && !e.repeat ) { 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()); location.reload(); } 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); location.reload(); } else { alert(text.intervalError); } }); // 註冊油猴右鍵選單:語言切換 GM_registerMenuCommand(text.langSwitch, () => { GM_setValue("lang", lang === "en" ? "zh-tw" : "en"); location.reload(); }); // 啟動腳本 init(); })();