- // ==UserScript==
- // @name YouTube Enhancer (Loop & Screenshot Button)
- // @description Integrating loop and screenshot buttons into the video and shorts player to enhance user functionality.
- // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
- // @version 1.0
- // @author exyezed
- // @namespace https://github.com/exyezed/youtube-enhancer/
- // @supportURL https://github.com/exyezed/youtube-enhancer/issues
- // @license MIT
- // @match https://www.youtube.com/*
- // @grant GM_xmlhttpRequest
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // Configuration
- const YouTubeEnhancerLoopScreenshotConfig = {
- screenshotFormat: "png",
- extension: 'png',
- screenshotFunctionality: 2 // 0: download, 1: clipboard, 2: both
- };
-
- // CSS Styles
- const YouTubeEnhancerLoopScreenshotCSS = `
- @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
-
- a.YouTubeEnhancerLoopScreenshot-loop-button, a.YouTubeEnhancerLoopScreenshot-screenshot-button {
- text-align: center;
- }
-
- a.YouTubeEnhancerLoopScreenshot-loop-button svg, a.YouTubeEnhancerLoopScreenshot-screenshot-button svg {
- margin-bottom: 2px;
- width: 52%;
- vertical-align: middle;
- }
-
- a.YouTubeEnhancerLoopScreenshot-loop-button.active svg {
- fill: #ff0000;
- }
-
- a.YouTubeEnhancerLoopScreenshot-screenshot-button.clicked svg {
- fill: #ff0000;
- }
-
- .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-top: 16px;
- width: 48px;
- height: 48px;
- border-radius: 50%;
- cursor: pointer;
- transition: background-color 0.3s;
- }
-
- .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button .material-symbols-outlined {
- font-size: 24px;
- font-variation-settings: 'FILL' 1;
- }
-
- /* Theme-specific styles */
- html[dark] .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button {
- background-color: rgba(255, 255, 255, 0.1);
- }
-
- html[dark] .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button:hover {
- background-color: rgba(255, 255, 255, 0.2);
- }
-
- html[dark] .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button .material-symbols-outlined {
- color: white;
- }
-
- html:not([dark]) .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button {
- background-color: rgba(0, 0, 0, 0.05);
- }
-
- html:not([dark]) .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button:hover {
- background-color: rgba(0, 0, 0, 0.1);
- }
-
- html:not([dark]) .YouTubeEnhancerLoopScreenshot-shorts-screenshot-button .material-symbols-outlined {
- color: #030303;
- }
- `;
-
- // Utility Functions
- const YouTubeEnhancerLoopScreenshotUtils = {
- addStyle(styleString) {
- const style = document.createElement('style');
- style.textContent = styleString;
- document.head.append(style);
- },
-
- getYouTubeVideoLoopSVG() {
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
- svg.setAttribute('height', '24px');
- svg.setAttribute('viewBox', '0 -960 960 960');
- svg.setAttribute('width', '24px');
- svg.setAttribute('fill', '#e8eaed');
-
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- 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');
-
- svg.appendChild(path);
- return svg;
- },
-
- getYouTubeVideoScreenshotSVG() {
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
- svg.setAttribute('height', '24px');
- svg.setAttribute('viewBox', '0 -960 960 960');
- svg.setAttribute('width', '24px');
- svg.setAttribute('fill', '#e8eaed');
-
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- 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');
-
- svg.appendChild(path);
- return svg;
- },
-
- getVideoId() {
- const urlParams = new URLSearchParams(window.location.search);
- return urlParams.get('v') || window.location.pathname.split('/').pop();
- },
-
- getVideoTitle(callback) {
- const videoId = this.getVideoId();
- GM_xmlhttpRequest({
- method: "GET",
- url: `https://exyezed.vercel.app/api/video/${videoId}`,
- onload: function(response) {
- if (response.status === 200) {
- const data = JSON.parse(response.responseText);
- callback(data.title);
- } else {
- callback('YouTube Video');
- }
- },
- onerror: function() {
- callback('YouTube Video');
- }
- });
- },
-
- formatTime(time) {
- const date = new Date();
- const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
- const timeString = [
- Math.floor(time / 3600),
- Math.floor((time % 3600) / 60),
- Math.floor(time % 60)
- ].map(v => v.toString().padStart(2, '0')).join('-');
- return `${dateString} ${timeString}`;
- },
-
- async copyToClipboard(blob) {
- const clipboardItem = new ClipboardItem({ "image/png": blob });
- await navigator.clipboard.write([clipboardItem]);
- },
-
- downloadScreenshot(blob, filename) {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.style.display = 'none';
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
- };
-
- // Regular YouTube Video Functions
- const YouTubeEnhancerLoopScreenshotRegularVideo = {
- init() {
- this.insertLoopElement();
- this.insertScreenshotElement();
- this.addObserver();
- this.addContextMenuListener();
- },
-
- insertLoopElement() {
- const newButton = document.createElement('a');
- newButton.classList.add('ytp-button', 'YouTubeEnhancerLoopScreenshot-loop-button');
- newButton.title = 'Loop Video';
- newButton.appendChild(YouTubeEnhancerLoopScreenshotUtils.getYouTubeVideoLoopSVG());
- newButton.addEventListener('click', this.toggleLoopState);
-
- document.querySelector('div.ytp-left-controls').appendChild(newButton);
- },
-
- insertScreenshotElement() {
- const newButton = document.createElement('a');
- newButton.classList.add('ytp-button', 'YouTubeEnhancerLoopScreenshot-screenshot-button');
- newButton.title = 'Take Screenshot';
- newButton.appendChild(YouTubeEnhancerLoopScreenshotUtils.getYouTubeVideoScreenshotSVG());
- newButton.addEventListener('click', this.handleScreenshotClick);
-
- const loopButton = document.querySelector('.YouTubeEnhancerLoopScreenshot-loop-button');
- loopButton.parentNode.insertBefore(newButton, loopButton.nextSibling);
- },
-
- toggleLoopState() {
- const video = document.querySelector('video');
- video.loop = !video.loop;
- if (video.loop) video.play();
-
- YouTubeEnhancerLoopScreenshotRegularVideo.updateToggleControls();
- },
-
- updateToggleControls() {
- const youtubeVideoLoop = document.querySelector('.YouTubeEnhancerLoopScreenshot-loop-button');
- youtubeVideoLoop.classList.toggle('active');
- youtubeVideoLoop.setAttribute('title', this.isActive() ? 'Stop Looping' : 'Loop Video');
- },
-
- isActive() {
- const youtubeVideoLoop = document.querySelector('.YouTubeEnhancerLoopScreenshot-loop-button');
- return youtubeVideoLoop.classList.contains('active');
- },
-
- addObserver() {
- const video = document.querySelector('video');
- new MutationObserver((mutations) => {
- mutations.forEach(() => {
- if ((video.getAttribute('loop') === null && this.isActive()) ||
- (video.getAttribute('loop') !== null && !this.isActive())) this.updateToggleControls();
- });
- }).observe(video, { attributes: true, attributeFilter: ['loop'] });
- },
-
- addContextMenuListener() {
- const video = document.querySelector('video');
- video.addEventListener('contextmenu', () => {
- setTimeout(() => {
- const checkbox = document.querySelector('[role=menuitemcheckbox]');
- checkbox.setAttribute('aria-checked', this.isActive());
- checkbox.addEventListener('click', this.toggleLoopState);
- }, 50);
- });
- },
-
- handleScreenshotClick(event) {
- const button = event.currentTarget;
- button.classList.add('clicked');
- setTimeout(() => {
- button.classList.remove('clicked');
- }, 100);
-
- YouTubeEnhancerLoopScreenshotRegularVideo.captureScreenshot();
- },
-
- captureScreenshot() {
- const player = document.querySelector('video');
- if (!player) return;
-
- const canvas = document.createElement("canvas");
- canvas.width = player.videoWidth;
- canvas.height = player.videoHeight;
- canvas.getContext('2d').drawImage(player, 0, 0, canvas.width, canvas.height);
-
- YouTubeEnhancerLoopScreenshotUtils.getVideoTitle((title) => {
- const time = player.currentTime;
- const filename = `${title} ${YouTubeEnhancerLoopScreenshotUtils.formatTime(time)}.${YouTubeEnhancerLoopScreenshotConfig.extension}`;
-
- canvas.toBlob(async (blob) => {
- if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 1 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
- await YouTubeEnhancerLoopScreenshotUtils.copyToClipboard(blob);
- }
- if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 0 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
- YouTubeEnhancerLoopScreenshotUtils.downloadScreenshot(blob, filename);
- }
- }, `image/${YouTubeEnhancerLoopScreenshotConfig.screenshotFormat}`);
- });
- }
- };
-
- // YouTube Shorts Functions
- const YouTubeEnhancerLoopScreenshotShorts = {
- init() {
- this.insertScreenshotElement();
- },
-
- insertScreenshotElement() {
- const shortsContainer = document.querySelector('ytd-reel-video-renderer[is-active] #actions');
- if (shortsContainer && !shortsContainer.querySelector('.YouTubeEnhancerLoopScreenshot-shorts-screenshot-button')) {
- const iconDiv = document.createElement('div');
- iconDiv.className = 'YouTubeEnhancerLoopScreenshot-shorts-screenshot-button';
-
- const iconSpan = document.createElement('span');
- iconSpan.className = 'material-symbols-outlined';
- iconSpan.textContent =
-
- 'photo_camera_back';
-
- iconDiv.appendChild(iconSpan);
-
- const customShortsIcon = shortsContainer.querySelector('#custom-shorts-icon');
- if (customShortsIcon) {
- customShortsIcon.parentNode.insertBefore(iconDiv, customShortsIcon);
- } else {
- shortsContainer.insertBefore(iconDiv, shortsContainer.firstChild);
- }
-
- iconDiv.addEventListener('click', () => this.captureScreenshot());
- }
- },
-
- captureScreenshot() {
- const player = document.querySelector('ytd-reel-video-renderer[is-active] video');
- if (!player) return;
-
- const canvas = document.createElement("canvas");
- canvas.width = player.videoWidth;
- canvas.height = player.videoHeight;
- canvas.getContext('2d').drawImage(player, 0, 0, canvas.width, canvas.height);
-
- YouTubeEnhancerLoopScreenshotUtils.getVideoTitle((title) => {
- const time = player.currentTime;
- const filename = `${title} ${YouTubeEnhancerLoopScreenshotUtils.formatTime(time)}.${YouTubeEnhancerLoopScreenshotConfig.extension}`;
-
- canvas.toBlob(async (blob) => {
- if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 1 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
- await YouTubeEnhancerLoopScreenshotUtils.copyToClipboard(blob);
- }
- if (YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 0 || YouTubeEnhancerLoopScreenshotConfig.screenshotFunctionality === 2) {
- YouTubeEnhancerLoopScreenshotUtils.downloadScreenshot(blob, filename);
- }
- }, `image/${YouTubeEnhancerLoopScreenshotConfig.screenshotFormat}`);
- });
- }
- };
-
- // Theme Functions
- const YouTubeEnhancerLoopScreenshotTheme = {
- init() {
- this.updateStyles();
- this.addObserver();
- },
-
- updateStyles() {
- const isDarkTheme = document.documentElement.hasAttribute('dark');
- document.documentElement.classList.toggle('dark-theme', isDarkTheme);
- },
-
- addObserver() {
- const observer = new MutationObserver(this.updateStyles);
- observer.observe(document.documentElement, {
- attributes: true,
- attributeFilter: ['dark']
- });
- }
- };
-
- // Main Initialization
- function YouTubeEnhancerLoopScreenshotInit() {
- YouTubeEnhancerLoopScreenshotUtils.addStyle(YouTubeEnhancerLoopScreenshotCSS);
- setTimeout(YouTubeEnhancerLoopScreenshotVideoElementPresent() ? YouTubeEnhancerLoopScreenshotInitializeFeatures : YouTubeEnhancerLoopScreenshotInit, 500);
- }
-
- function YouTubeEnhancerLoopScreenshotVideoElementPresent() {
- return document.querySelector('video') !== null;
- }
-
- function YouTubeEnhancerLoopScreenshotInitializeFeatures() {
- YouTubeEnhancerLoopScreenshotRegularVideo.init();
- YouTubeEnhancerLoopScreenshotTheme.init();
- YouTubeEnhancerLoopScreenshotInitializeShortsFeatures();
- }
-
- function YouTubeEnhancerLoopScreenshotInitializeShortsFeatures() {
- if (window.location.pathname.includes('/shorts/')) {
- setTimeout(YouTubeEnhancerLoopScreenshotShorts.init.bind(YouTubeEnhancerLoopScreenshotShorts), 500);
- }
- }
-
- // Observers and Event Listeners
- const YouTubeEnhancerLoopScreenshotShortsObserver = new MutationObserver((mutations) => {
- for (let mutation of mutations) {
- if (mutation.type === 'childList') {
- YouTubeEnhancerLoopScreenshotInitializeShortsFeatures();
- }
- }
- });
-
- YouTubeEnhancerLoopScreenshotShortsObserver.observe(document.body, { childList: true, subtree: true });
-
- window.addEventListener('yt-navigate-finish', YouTubeEnhancerLoopScreenshotInitializeShortsFeatures);
-
- document.addEventListener('yt-action', function(event) {
- if (event.detail && event.detail.actionName === 'yt-reload-continuation-items-command') {
- YouTubeEnhancerLoopScreenshotInitializeShortsFeatures();
- }
- });
-
- // Initialize
- YouTubeEnhancerLoopScreenshotInit();
- console.log('YouTubeEnhancerLoopScreenshot: Enjoy the awesome music and capture your favorite moments!');
- })();