Youtube Screenshot Button

Add a screenshot button for YouTube. Originally created by LeKAKiD, modified by me.

  1. // ==UserScript==
  2. // @name Youtube Screenshot Button
  3. // @namespace http://tampermonkey.net/
  4. // @match https://www.youtube.com/*
  5. // @version 1.01
  6. // @author LeKAKiD, Yukiteru
  7. // @description Add a screenshot button for YouTube. Originally created by LeKAKiD, modified by me.
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11. function handleYTFrame() {
  12. // Player elements
  13. let settingButton = undefined;
  14. let player = undefined;
  15. let video = undefined;
  16. const tooltip = {
  17. element: undefined,
  18. text: undefined,
  19. };
  20.  
  21. // Initialize element;
  22. const canvas = document.createElement("canvas");
  23. const context = canvas.getContext("2d", { alpha: false });
  24. const anchor = document.createElement("a");
  25.  
  26. // Render buttons
  27. const screenshotButton = document.createElement("button");
  28. screenshotButton.classList.add("ytp-button");
  29.  
  30. // Create the SVG element. Can't use innerHTML due to security update
  31. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  32. svg.setAttribute("fill", "none");
  33. svg.setAttribute("height", "100%");
  34. svg.setAttribute("viewBox", "-4 -4 28 28");
  35. svg.setAttribute("width", "100%");
  36.  
  37. // Path elements
  38. const pathData = [
  39. "M6.5 5C5.67157 5 5 5.67157 5 6.5V8.5C5 8.77614 5.22386 9 5.5 9C5.77614 9 6 8.77614 6 8.5V6.5C6 6.22386 6.22386 6 6.5 6H8.5C8.77614 6 9 5.77614 9 5.5C9 5.22386 8.77614 5 8.5 5H6.5Z",
  40. "M11.5 5C11.2239 5 11 5.22386 11 5.5C11 5.77614 11.2239 6 11.5 6H13.5C13.7761 6 14 6.22386 14 6.5V8.5C14 8.77614 14.2239 9 14.5 9C14.7761 9 15 8.77614 15 8.5V6.5C15 5.67157 14.3284 5 13.5 5H11.5Z",
  41. "M6 11.5C6 11.2239 5.77614 11 5.5 11C5.22386 11 5 11.2239 5 11.5V13.5C5 14.3284 5.67157 15 6.5 15H8.5C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14H6.5C6.22386 14 6 13.7761 6 13.5V11.5Z",
  42. "M15 11.5C15 11.2239 14.7761 11 14.5 11C14.2239 11 14 11.2239 14 11.5V13.5C14 13.7761 13.7761 14 13.5 14H11.5C11.2239 14 11 14.2239 11 14.5C11 14.7761 11.2239 15 11.5 15H13.5C14.3284 15 15 14.3284 15 13.5V11.5Z",
  43. "M3 5C3 3.89543 3.89543 3 5 3H15C16.1046 3 17 3.89543 17 5V15C17 16.1046 16.1046 17 15 17H5C3.89543 17 3 16.1046 3 15V5ZM4 5V15C4 15.5523 4.44772 16 5 16H15C15.5523 16 16 15.5523 16 15V5C16 4.44772 15.5523 4 15 4H5C4.44772 4 4 4.44772 4 5Z"
  44. ];
  45.  
  46. pathData.forEach(d => {
  47. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  48. path.setAttribute("d", d);
  49. path.setAttribute("fill", "#fff");
  50. svg.appendChild(path);
  51. });
  52.  
  53. // Append the SVG to the button
  54. screenshotButton.appendChild(svg);
  55.  
  56. // Capture function
  57. function capture() {
  58. if (!video) return null;
  59. canvas.width = video.videoWidth;
  60. canvas.height = video.videoHeight;
  61. context.drawImage(video, 0, 0);
  62. return new Promise(resolve => {
  63. canvas.toBlob(blob => resolve(blob));
  64. });
  65. }
  66.  
  67. // Tooltip control
  68. function showTooltip(text, referenceButton, withoutEvent) {
  69. // Borrow existing button event for animation
  70. if (!withoutEvent) settingButton.dispatchEvent(new MouseEvent("mouseover"));
  71. if (!player) {
  72. player = document.querySelector("#player:not(.skeleton)");
  73. tooltip.element = document.querySelector(".ytp-tooltip-text-wrapper").parentElement;
  74. tooltip.text = tooltip.element.querySelector(".ytp-tooltip-text");
  75. }
  76. tooltip.text.textContent = text;
  77. tooltip.element.style.left = "0px";
  78. const buttonRect = referenceButton.getBoundingClientRect();
  79. const tooltipRect = tooltip.element.getBoundingClientRect();
  80. const playerRect = player.getBoundingClientRect();
  81. const buttonRelativePos = buttonRect.x - playerRect.x;
  82. const buttonRelativeCenter = buttonRelativePos + buttonRect.width / 2;
  83. const left = buttonRelativeCenter - tooltipRect.width / 2;
  84. tooltip.element.style.left = `${left}px`;
  85. }
  86.  
  87. function hideTooltip() {
  88. // Borrow existing button event for animation
  89. settingButton.dispatchEvent(new MouseEvent("mouseout"));
  90. }
  91.  
  92. screenshotButton.addEventListener("click", async e => {
  93. const blob = await capture();
  94. if (!blob) return;
  95.  
  96. // copy image to clipboard
  97. const item = new window.ClipboardItem({ [blob.type]: blob });
  98. navigator.clipboard.write([item]);
  99.  
  100. // save image locally
  101. const url = URL.createObjectURL(blob);
  102. const ext = blob.type.split("/")[1];
  103. const filename = getFilename(ext);
  104. anchor.href = url;
  105. anchor.download = filename;
  106. anchor.click();
  107. URL.revokeObjectURL(url);
  108. });
  109.  
  110. function detectUserLanguage() {
  111. const language = navigator.language || navigator.userLanguage;
  112. return language.split("-")[0];
  113. }
  114.  
  115. function getTooltip() {
  116. const language = detectUserLanguage();
  117. const tooltipTexts = {
  118. en: "Screenshot",
  119. zh: "截图",
  120. ja: "スクリーンショット",
  121. ko: "스크린샷",
  122. };
  123.  
  124. return tooltipTexts[language];
  125. }
  126.  
  127. screenshotButton.addEventListener("mouseover", () => {
  128. const tooltip = getTooltip();
  129. showTooltip(tooltip, screenshotButton);
  130. });
  131.  
  132. screenshotButton.addEventListener("mouseout", () => {
  133. hideTooltip();
  134. });
  135.  
  136. // Observer
  137. const observer = new MutationObserver(() => {
  138. settingButton = document.querySelector(".ytp-right-controls > .ytp-settings-button");
  139. if (settingButton) {
  140. video = document.querySelector("video");
  141. settingButton.insertAdjacentElement("beforebegin", screenshotButton);
  142. observer.disconnect();
  143. return;
  144. }
  145. });
  146.  
  147. observer.observe(document.documentElement, {
  148. childList: true,
  149. subtree: true,
  150. });
  151. }
  152.  
  153. function getFilename(ext) {
  154. // can't use #title directly since there might be a mini player with a h1 element
  155. const title = document
  156. .querySelector(".ytd-watch-metadata")
  157. .querySelector("#title")
  158. .textContent.trim();
  159. const time = new Date().toLocaleString().replaceAll("/", "-");
  160. const progress = document.querySelector("video.video-stream").currentTime.toFixed(2);
  161. return (filename = `${title} ${progress} ${time} screenshot.${ext}`);
  162. }
  163.  
  164. const urlRegex = /https:\/\/(.+\.)?youtube\.com\/embed\/.+/;
  165. function addAllowClipboard() {
  166. document.querySelectorAll("iframe").forEach(e => {
  167. if (urlRegex.test(e.src) && e.allow.indexOf("clipboard-write") === -1) {
  168. e.allow += "clipboard-write;";
  169. e.src = e.src;
  170. }
  171. });
  172. }
  173.  
  174. if (window.self === window.top) addAllowClipboard();
  175.  
  176. const ytRegex = /https:\/\/(.+\.)?youtube\.com(\/(embed|watch).+)?/;
  177. if (ytRegex.test(location.href)) handleYTFrame();