YouTube Repeat Button

Adding YouTube Repeat Button

  1. // ==UserScript==
  2. // @name YouTube Repeat Button
  3. // @namespace https://greasyfork.org/fr/users/1528785
  4. // @version 1.0
  5. // @description Adding YouTube Repeat Button
  6. // @author rommar31
  7. // @match https://www.youtube.com/*
  8. // @icon https://cdn-icons-png.flaticon.com/32/1384/1384060.png
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. let repeatEnabled = false;
  17. let currentVideo = null;
  18. let endedHandler = null;
  19.  
  20. function logError(context, error) {
  21. console.warn(`[YouTube Repeat Button] ${context}:`, error);
  22. }
  23.  
  24. function createRepeatIcon() {
  25. const ns = "http://www.w3.org/2000/svg";
  26. const svg = document.createElementNS(ns, "svg");
  27. svg.setAttribute("viewBox", "0 0 24 24");
  28. svg.setAttribute("width", "28");
  29. svg.setAttribute("height", "28");
  30. svg.setAttribute("fill", "white");
  31.  
  32. const path1 = document.createElementNS(ns, "path");
  33. path1.setAttribute("d", "M0 0h24v24H0V0z");
  34. path1.setAttribute("fill", "none");
  35.  
  36. const path2 = document.createElementNS(ns, "path");
  37. path2.setAttribute("d", "M7 7h10v3l4-4-4-4v3H6c-1.1 0-2 .9-2 2v5h2V7zm10 10H7v-3l-4 4 4 4v-3h11c1.1 0 2-.9 2-2v-5h-2v5z");
  38.  
  39. svg.appendChild(path1);
  40. svg.appendChild(path2);
  41. return svg;
  42. }
  43.  
  44. function createCheckIcon() {
  45. const ns = "http://www.w3.org/2000/svg";
  46. const svg = document.createElementNS(ns, "svg");
  47. svg.setAttribute("viewBox", "0 0 24 24");
  48. svg.setAttribute("width", "20");
  49. svg.setAttribute("height", "20");
  50. svg.setAttribute("fill", "limegreen");
  51. svg.style.position = "absolute";
  52. svg.style.top = "2px";
  53. svg.style.right = "0px";
  54.  
  55. const path1 = document.createElementNS(ns, "path");
  56. path1.setAttribute("d", "M0 0h24v24H0z");
  57. path1.setAttribute("fill", "none");
  58.  
  59. const path2 = document.createElementNS(ns, "path");
  60. path2.setAttribute("d", "M9 16.17l-3.59-3.58L4 14l5 5 12-12-1.41-1.42z");
  61.  
  62. svg.appendChild(path1);
  63. svg.appendChild(path2);
  64. return svg;
  65. }
  66.  
  67. function detachFromCurrentVideo() {
  68. if (!currentVideo) return;
  69. try {
  70. currentVideo.loop = false;
  71. if (endedHandler) currentVideo.removeEventListener('ended', endedHandler);
  72. } catch (err) {
  73. logError("detachFromCurrentVideo", err);
  74. } finally {
  75. currentVideo = null;
  76. endedHandler = null;
  77. }
  78. currentVideo = null;
  79. endedHandler = null;
  80. }
  81.  
  82. function attachToVideo(videoEl) {
  83. if (!videoEl || currentVideo === videoEl) return;
  84.  
  85. detachFromCurrentVideo();
  86. currentVideo = videoEl;
  87.  
  88. try {
  89. currentVideo.loop = repeatEnabled;
  90. } catch (err) {
  91. logError("set video.loop", err);
  92. }
  93.  
  94. endedHandler = function() {
  95. if (!repeatEnabled) return;
  96. try {
  97. currentVideo.currentTime = 0;
  98. const p = currentVideo.play();
  99. if (p && typeof p.catch === 'function') {
  100. p.catch(err => logError("video.play()", err));
  101. }
  102. } catch (err) {
  103. logError("endedHandler", err);
  104. }
  105. };
  106.  
  107. try {
  108. currentVideo.addEventListener('ended', endedHandler);
  109. } catch (err) {
  110. logError("addEventListener(ended)", err);
  111. }
  112. }
  113.  
  114.  
  115. function addRepeatButton() {
  116. if (document.querySelector('.ytp-repeat-button')) return;
  117.  
  118. const button = document.createElement('button');
  119. button.className = 'ytp-button ytp-repeat-button';
  120. button.title = 'Repeat';
  121. button.style.width = '40px';
  122. button.style.height = '40px';
  123. button.style.background = 'transparent';
  124. button.style.border = 'none';
  125. button.style.cursor = 'pointer';
  126. button.style.position = 'relative';
  127. button.style.display = 'flex';
  128. button.style.alignItems = 'center';
  129. button.style.justifyContent = 'center';
  130. button.style.padding = '0';
  131.  
  132. const repeatIcon = createRepeatIcon();
  133. const checkIcon = createCheckIcon();
  134. checkIcon.style.display = repeatEnabled ? 'block' : 'none';
  135.  
  136. button.appendChild(repeatIcon);
  137. button.appendChild(checkIcon);
  138.  
  139. button.addEventListener('click', () => {
  140. repeatEnabled = !repeatEnabled;
  141. checkIcon.style.display = repeatEnabled ? 'block' : 'none';
  142.  
  143. const v = document.querySelector('video');
  144. if (v) {
  145. try {
  146. v.loop = !!repeatEnabled;
  147. } catch (e) {}
  148. attachToVideo(v);
  149. }
  150. });
  151.  
  152. const controls = document.querySelector('.ytp-right-controls');
  153. if (controls) {
  154. controls.insertBefore(button, controls.firstChild);
  155. }
  156. }
  157.  
  158. const globalObserver = new MutationObserver((mutations) => {
  159. addRepeatButton();
  160.  
  161. const v = document.querySelector('video');
  162. if (v) attachToVideo(v);
  163. });
  164. globalObserver.observe(document.body, { childList: true, subtree: true });
  165.  
  166. addRepeatButton();
  167. const initialVideo = document.querySelector('video');
  168. if (initialVideo) attachToVideo(initialVideo);
  169.  
  170. setInterval(() => {
  171. const v = document.querySelector('video');
  172. if (v && v !== currentVideo) attachToVideo(v);
  173. }, 800);
  174.  
  175. window.addEventListener('beforeunload', () => detachFromCurrentVideo());
  176. })();