您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
動画のスクリーンショットを撮るキーボードショートカットを追加する
当前为
// ==UserScript== // @name YouTube: Take screenshots using hotkey // @description 動画のスクリーンショットを撮るキーボードショートカットを追加する // @namespace https://gitlab.com/sigsign // @version 0.2.0 // @author Sigsign // @license MIT or Apache-2.0 // @match https://www.youtube.com/* // @grant none // ==/UserScript== (function () { 'use strict'; function getPlayer() { return document.querySelector('#movie_player'); } const notifications = new Set(); function capture(video) { return new Promise((resolv, reject) => { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); if (!ctx) { return reject('[Err] canvas.getContext() is failed'); } ctx.drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob((blob) => { if (!blob) { return reject('[Err] blob is null'); } resolv(blob); }, 'image/png'); }); } // [Firefox] dom.events.asyncClipboard.clipboardItem => True async function copyToClipboard(blob) { // eslint-disable-next-line no-undef const clipboardItem = new ClipboardItem({ [blob.type]: blob }); await navigator.clipboard.write([clipboardItem]); } function getIcon() { const icon = document.querySelector('ytd-video-owner-renderer img'); return icon ? icon.src : ''; } function getTitle() { const player = getPlayer(); if (!player || typeof player.getVideoData !== 'function') { return ''; } const data = player.getVideoData() || {}; return data.title || ''; } async function notify(title, icon, image) { if (Notification.permission === 'denied') { return; } if (Notification.permission !== 'granted') { if (await Notification.requestPermission() !== 'granted') { return; } } for (const n of notifications) { n.close(); } notifications.clear(); const url = URL.createObjectURL(image); const options = { body: title, icon: icon, image: url, silent: true, }; const n = new Notification('Take a screenshot', options); setTimeout(() => { n.close(); }, 3 * 1000); n.onclose = () => { URL.revokeObjectURL(url); notifications.delete(n); }; notifications.add(n); } function readyStream(list) { if (list.contains('playing-mode') || list.contains('paused-mode')) { return true; } return false; } async function takeScreenshot() { const player = getPlayer(); if (!player || !readyStream(player.classList)) { throw new Error('[Err] YouTube Player is not ready'); } const video = player.querySelector('video'); if (!video) { throw new Error('[Err] HTMLVideoElement is not exists'); } const blob = await capture(video); copyToClipboard(blob); const text = getTitle(); const icon = getIcon(); notify(text, icon, blob); } function init() { const path = location.pathname; // チャット欄にフォーカスがあってもスクショを撮れるようにする if (path.includes('/live_chat') || path.includes('/live_chat_replay')) { window.addEventListener('keydown', function (ev) { if (ev.target === window.document.body && ev.key === 'q') { const message = { type: 'userscript-take-screenshots' }; window.parent.postMessage(message); } }, false); // チャット欄でやることはもうない return; } // 最上位のフレームだけが postMessage() を受け取る if (window.top === window.self) { window.addEventListener('message', (ev) => { const url = new URL(ev.origin); if (url.hostname !== 'www.youtube.com' || !ev.data) { return; } const message = ev.data; if (message.type === 'userscript-take-screenshots') { takeScreenshot().catch((err) => { console.error(err); }); } }); } window.addEventListener('keydown', (ev) => { if (ev.key === 'q') { takeScreenshot().catch((err) => { console.error(err); }); } }, false); } init(); })();