YouTube Enhancer (Loop & Screenshot Button)

Integrating loop and screenshot buttons into the video and shorts player to enhance user functionality.

目前為 2024-10-18 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Loop & Screenshot Button)
  3. // @description Integrating loop and screenshot buttons into the video and shorts player to enhance user functionality.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.0
  6. // @author exyezed
  7. // @namespace https://github.com/exyezed/youtube-enhancer/
  8. // @supportURL https://github.com/exyezed/youtube-enhancer/issues
  9. // @license MIT
  10. // @match https://www.youtube.com/*
  11. // @grant GM_xmlhttpRequest
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Configuration
  18. const YouTubeEnhancerLoopScreenshotConfig = {
  19. screenshotFormat: "png",
  20. extension: 'png',
  21. screenshotFunctionality: 2 // 0: download, 1: clipboard, 2: both
  22. };
  23.  
  24. // CSS Styles
  25. const YouTubeEnhancerLoopScreenshotCSS = `
  26. @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
  27.  
  28. a.YouTubeEnhancerLoopScreenshot-loop-button, a.YouTubeEnhancerLoopScreenshot-screenshot-button {
  29. text-align: center;
  30. }
  31.  
  32. a.YouTubeEnhancerLoopScreenshot-loop-button svg, a.YouTubeEnhancerLoopScreenshot-screenshot-button svg {
  33. margin-bottom: 2px;
  34. width: 52%;
  35. vertical-align: middle;
  36. }
  37.  
  38. a.YouTubeEnhancerLoopScreenshot-loop-button.active svg {
  39. fill: #ff0000;
  40. }
  41.  
  42. a.YouTubeEnhancerLoopScreenshot-screenshot-button.clicked svg {
  43. fill: #ff0000;
  44. }
  45.  
  46. .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button {
  47. display: flex;
  48. align-items: center;
  49. justify-content: center;
  50. margin-top: 16px;
  51. width: 48px;
  52. height: 48px;
  53. border-radius: 50%;
  54. cursor: pointer;
  55. transition: background-color 0.3s;
  56. }
  57.  
  58. .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button .material-symbols-outlined {
  59. font-size: 24px;
  60. font-variation-settings: 'FILL' 1;
  61. }
  62.  
  63. /* Theme-specific styles */
  64. html[dark] .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button {
  65. background-color: rgba(255, 255, 255, 0.1);
  66. }
  67.  
  68. html[dark] .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button:hover {
  69. background-color: rgba(255, 255, 255, 0.2);
  70. }
  71.  
  72. html[dark] .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button .material-symbols-outlined {
  73. color: white;
  74. }
  75.  
  76. html:not([dark]) .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button {
  77. background-color: rgba(0, 0, 0, 0.05);
  78. }
  79.  
  80. html:not([dark]) .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button:hover {
  81. background-color: rgba(0, 0, 0, 0.1);
  82. }
  83.  
  84. html:not([dark]) .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button .material-symbols-outlined {
  85. color: #030303;
  86. }
  87. `;
  88.  
  89. // Utility Functions
  90. const YouTubeEnhancerLoopScreenshotUtils = {
  91. addStyle(styleString) {
  92. const style = document.createElement('style');
  93. style.textContent = styleString;
  94. document.head.append(style);
  95. },
  96.  
  97. getYouTubeVideoLoopSVG() {
  98. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  99. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  100. svg.setAttribute('height', '24px');
  101. svg.setAttribute('viewBox', '0 -960 960 960');
  102. svg.setAttribute('width', '24px');
  103. svg.setAttribute('fill', '#e8eaed');
  104. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  105. path.setAttribute('d', 'M220-260q-92 0-156-64T0-480q0-92 64-156t156-64q37 0 71 13t61 37l68 62-60 54-62-56q-16-14-36-22t-42-8q-58 0-99 41t-41 99q0 58 41 99t99 41q22 0 42-8t36-22l310-280q27-24 61-37t71-13q92 0 156 64t64 156q0 92-64 156t-156 64q-37 0-71-13t-61-37l-68-62 60-54 62 56q16 14 36 22t42 8q58 0 99-41t41-99q0-58-41-99t-99-41q-22 0-42 8t-36 22L352-310q-27 24-61 37t-71 13Z');
  106. svg.appendChild(path);
  107. return svg;
  108. },
  109.  
  110. getYouTubeVideoScreenshotSVG() {
  111. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  112. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  113. svg.setAttribute('height', '24px');
  114. svg.setAttribute('viewBox', '0 -960 960 960');
  115. svg.setAttribute('width', '24px');
  116. svg.setAttribute('fill', '#e8eaed');
  117. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  118. path.setAttribute('d', 'M240-280h480L570-480 450-320l-90-120-120 160Zm-80 160q-33 0-56.5-23.5T80-200v-480q0-33 23.5-56.5T160-760h126l74-80h240l74 80h126q33 0 56.5 23.5T880-680v480q0 33-23.5 56.5T800-120H160Zm0-80h640v-480H638l-73-80H395l-73 80H160v480Zm320-240Z');
  119. svg.appendChild(path);
  120. return svg;
  121. },
  122.  
  123. getVideoId() {
  124. const urlParams = new URLSearchParams(window.location.search);
  125. return urlParams.get('v') || window.location.pathname.split('/').pop();
  126. },
  127.  
  128. getVideoTitle(callback) {
  129. const videoId = this.getVideoId();
  130. GM_xmlhttpRequest({
  131. method: "GET",
  132. url: `https://exyezed.vercel.app/api/video/${videoId}`,
  133. onload: function(response) {
  134. if (response.status === 200) {
  135. const data = JSON.parse(response.responseText);
  136. callback(data.title);
  137. } else {
  138. callback('YouTube Video');
  139. }
  140. },
  141. onerror: function() {
  142. callback('YouTube Video');
  143. }
  144. });
  145. },
  146.  
  147. formatTime(time) {
  148. const date = new Date();
  149. const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
  150. const timeString = [
  151. Math.floor(time / 3600),
  152. Math.floor((time % 3600) / 60),
  153. Math.floor(time % 60)
  154. ].map(v => v.toString().padStart(2, '0')).join('-');
  155. return `${dateString} ${timeString}`;
  156. },
  157.  
  158. async copyToClipboard(blob) {
  159. const clipboardItem = new ClipboardItem({ "image/png": blob });
  160. await navigator.clipboard.write([clipboardItem]);
  161. },
  162.  
  163. downloadScreenshot(blob, filename) {
  164. const url = URL.createObjectURL(blob);
  165. const a = document.createElement('a');
  166. a.style.display = 'none';
  167. a.href = url;
  168. a.download = filename;
  169. document.body.appendChild(a);
  170. a.click();
  171. document.body.removeChild(a);
  172. URL.revokeObjectURL(url);
  173. }
  174. };
  175.  
  176. // Regular YouTube Video Functions
  177. const YouTubeEnhancerLoopScreenshotRegularVideo = {
  178. init() {
  179. this.insertLoopElement();
  180. this.insertScreenshotElement();
  181. this.addObserver();
  182. this.addContextMenuListener();
  183. },
  184.  
  185. insertLoopElement() {
  186. const newButton = document.createElement('a');
  187. newButton.classList.add('ytp-button', 'YouTubeEnhancerLoopScreenshot-loop-button');
  188. newButton.title = 'Loop Video';
  189. newButton.appendChild(YouTubeEnhancerLoopScreenshotUtils.getYouTubeVideoLoopSVG());
  190. newButton.addEventListener('click', this.toggleLoopState);
  191.  
  192. document.querySelector('div.ytp-left-controls').appendChild(newButton);
  193. },
  194.  
  195. insertScreenshotElement() {
  196. const newButton = document.createElement('a');
  197. newButton.classList.add('ytp-button', 'YouTubeEnhancerLoopScreenshot-screenshot-button');
  198. newButton.title = 'Take Screenshot';
  199. newButton.appendChild(YouTubeEnhancerLoopScreenshotUtils.getYouTubeVideoScreenshotSVG());
  200. newButton.addEventListener('click', this.handleScreenshotClick);
  201.  
  202. const loopButton = document.querySelector('.YouTubeEnhancerLoopScreenshot-loop-button');
  203. loopButton.parentNode.insertBefore(newButton, loopButton.nextSibling);
  204. },
  205.  
  206. toggleLoopState() {
  207. const video = document.querySelector('video');
  208. video.loop = !video.loop;
  209. if (video.loop) video.play();
  210.  
  211. YouTubeEnhancerLoopScreenshotRegularVideo.updateToggleControls();
  212. },
  213.  
  214. updateToggleControls() {
  215. const youtubeVideoLoop = document.querySelector('.YouTubeEnhancerLoopScreenshot-loop-button');
  216. youtubeVideoLoop.classList.toggle('active');
  217. youtubeVideoLoop.setAttribute('title', this.isActive() ? 'Stop Looping' : 'Loop Video');
  218. },
  219.  
  220. isActive() {
  221. const youtubeVideoLoop = document.querySelector('.YouTubeEnhancerLoopScreenshot-loop-button');
  222. return youtubeVideoLoop.classList.contains('active');
  223. },
  224.  
  225. addObserver() {
  226. const video = document.querySelector('video');
  227. new MutationObserver((mutations) => {
  228. mutations.forEach(() => {
  229. if ((video.getAttribute('loop') === null && this.isActive()) ||
  230. (video.getAttribute('loop') !== null && !this.isActive())) this.updateToggleControls();
  231. });
  232. }).observe(video, { attributes: true, attributeFilter: ['loop'] });
  233. },
  234.  
  235. addContextMenuListener() {
  236. const video = document.querySelector('video');
  237. video.addEventListener('contextmenu', () => {
  238. setTimeout(() => {
  239. const checkbox = document.querySelector('[role=menuitemcheckbox]');
  240. checkbox.setAttribute('aria-checked', this.isActive());
  241. checkbox.addEventListener('click', this.toggleLoopState);
  242. }, 50);
  243. });
  244. },
  245.  
  246. handleScreenshotClick(event) {
  247. const button = event.currentTarget;
  248. button.classList.add('clicked');
  249. setTimeout(() => {
  250. button.classList.remove('clicked');
  251. }, 100);
  252.  
  253. YouTubeEnhancerLoopScreenshotRegularVideo.captureScreenshot();
  254. },
  255.  
  256. captureScreenshot() {
  257. const player = document.querySelector('video');
  258. if (!player) return;
  259.  
  260. const canvas = document.createElement("canvas");
  261. canvas.width = player.videoWidth;
  262. canvas.height = player.videoHeight;
  263. canvas.getContext('2d').drawImage(player, 0, 0, canvas.width, canvas.height);
  264.  
  265. YouTubeEnhancerLoopScreenshotUtils.getVideoTitle((title) => {
  266. const time = player.currentTime;
  267. const filename = `${title} ${YouTubeEnhancerLoopScreenshotUtils.formatTime(time)}.${YouTubeEnhancerLoopScreenshotConfig.extension}`;
  268.  
  269. canvas.toBlob(async (blob) => {
  270. if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 1 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
  271. await YouTubeEnhancerLoopScreenshotUtils.copyToClipboard(blob);
  272. }
  273. if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 0 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
  274. YouTubeEnhancerLoopScreenshotUtils.downloadScreenshot(blob, filename);
  275. }
  276. }, `image/${YouTubeEnhancerLoopScreenshotConfig.screenshotFormat}`);
  277. });
  278. }
  279. };
  280.  
  281. // YouTube Shorts Functions
  282. const YouTubeEnhancerLoopScreenshotShorts = {
  283. init() {
  284. this.insertScreenshotElement();
  285. },
  286.  
  287. insertScreenshotElement() {
  288. const shortsContainer = document.querySelector('ytd-reel-video-renderer[is-active] #actions');
  289. if (shortsContainer && !shortsContainer.querySelector('.YouTubeEnhancerLoopScreenshot-shorts-screenshot-button')) {
  290. const iconDiv = document.createElement('div');
  291. iconDiv.className = 'YouTubeEnhancerLoopScreenshot-shorts-screenshot-button';
  292.  
  293. const iconSpan = document.createElement('span');
  294. iconSpan.className = 'material-symbols-outlined';
  295. iconSpan.textContent =
  296.  
  297. 'photo_camera_back';
  298.  
  299. iconDiv.appendChild(iconSpan);
  300. const customShortsIcon = shortsContainer.querySelector('#custom-shorts-icon');
  301. if (customShortsIcon) {
  302. customShortsIcon.parentNode.insertBefore(iconDiv, customShortsIcon);
  303. } else {
  304. shortsContainer.insertBefore(iconDiv, shortsContainer.firstChild);
  305. }
  306.  
  307. iconDiv.addEventListener('click', () => this.captureScreenshot());
  308. }
  309. },
  310.  
  311. captureScreenshot() {
  312. const player = document.querySelector('ytd-reel-video-renderer[is-active] video');
  313. if (!player) return;
  314.  
  315. const canvas = document.createElement("canvas");
  316. canvas.width = player.videoWidth;
  317. canvas.height = player.videoHeight;
  318. canvas.getContext('2d').drawImage(player, 0, 0, canvas.width, canvas.height);
  319.  
  320. YouTubeEnhancerLoopScreenshotUtils.getVideoTitle((title) => {
  321. const time = player.currentTime;
  322. const filename = `${title} ${YouTubeEnhancerLoopScreenshotUtils.formatTime(time)}.${YouTubeEnhancerLoopScreenshotConfig.extension}`;
  323.  
  324. canvas.toBlob(async (blob) => {
  325. if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 1 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
  326. await YouTubeEnhancerLoopScreenshotUtils.copyToClipboard(blob);
  327. }
  328. if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 0 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
  329. YouTubeEnhancerLoopScreenshotUtils.downloadScreenshot(blob, filename);
  330. }
  331. }, `image/${YouTubeEnhancerLoopScreenshotConfig.screenshotFormat}`);
  332. });
  333. }
  334. };
  335.  
  336. // Theme Functions
  337. const YouTubeEnhancerLoopScreenshotTheme = {
  338. init() {
  339. this.updateStyles();
  340. this.addObserver();
  341. },
  342.  
  343. updateStyles() {
  344. const isDarkTheme = document.documentElement.hasAttribute('dark');
  345. document.documentElement.classList.toggle('dark-theme', isDarkTheme);
  346. },
  347.  
  348. addObserver() {
  349. const observer = new MutationObserver(this.updateStyles);
  350. observer.observe(document.documentElement, {
  351. attributes: true,
  352. attributeFilter: ['dark']
  353. });
  354. }
  355. };
  356.  
  357. // Main Initialization
  358. function YouTubeEnhancerLoopScreenshotInit() {
  359. YouTubeEnhancerLoopScreenshotUtils.addStyle(YouTubeEnhancerLoopScreenshotCSS);
  360. setTimeout(YouTubeEnhancerLoopScreenshotVideoElementPresent() ? YouTubeEnhancerLoopScreenshotInitializeFeatures : YouTubeEnhancerLoopScreenshotInit, 500);
  361. }
  362.  
  363. function YouTubeEnhancerLoopScreenshotVideoElementPresent() {
  364. return document.querySelector('video') !== null;
  365. }
  366.  
  367. function YouTubeEnhancerLoopScreenshotInitializeFeatures() {
  368. YouTubeEnhancerLoopScreenshotRegularVideo.init();
  369. YouTubeEnhancerLoopScreenshotTheme.init();
  370. YouTubeEnhancerLoopScreenshotInitializeShortsFeatures();
  371. }
  372.  
  373. function YouTubeEnhancerLoopScreenshotInitializeShortsFeatures() {
  374. if (window.location.pathname.includes('/shorts/')) {
  375. setTimeout(YouTubeEnhancerLoopScreenshotShorts.init.bind(YouTubeEnhancerLoopScreenshotShorts), 500);
  376. }
  377. }
  378.  
  379. // Observers and Event Listeners
  380. const YouTubeEnhancerLoopScreenshotShortsObserver = new MutationObserver((mutations) => {
  381. for (let mutation of mutations) {
  382. if (mutation.type === 'childList') {
  383. YouTubeEnhancerLoopScreenshotInitializeShortsFeatures();
  384. }
  385. }
  386. });
  387.  
  388. YouTubeEnhancerLoopScreenshotShortsObserver.observe(document.body, { childList: true, subtree: true });
  389.  
  390. window.addEventListener('yt-navigate-finish', YouTubeEnhancerLoopScreenshotInitializeShortsFeatures);
  391.  
  392. document.addEventListener('yt-action', function(event) {
  393. if (event.detail && event.detail.actionName === 'yt-reload-continuation-items-command') {
  394. YouTubeEnhancerLoopScreenshotInitializeShortsFeatures();
  395. }
  396. });
  397.  
  398. // Initialize
  399. YouTubeEnhancerLoopScreenshotInit();
  400. console.log('YouTubeEnhancerLoopScreenshot: Enjoy the awesome music and capture your favorite moments!');
  401. })();

QingJ © 2025

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