Youtube Mobile Looper

Add a simple loop/unloop button to the navbar for Youtube Mobile. Made in about 15 minutes as a teaching tutorial.

  1. // ==UserScript==
  2. // @name Youtube Mobile Looper
  3. // @namespace http://tampermonkey.net/
  4. // @version 422
  5. // @description Add a simple loop/unloop button to the navbar for Youtube Mobile. Made in about 15 minutes as a teaching tutorial.
  6. // @author Taylor Wright AKA Dildoer the Cocknight
  7. // @match https://m.youtube.com/*
  8. // @icon https://www.google.com/s2/favicons?domain=youtube.com
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. //Pretty much finished this script: I'm not going to touch it anymore now that it works perfectly. Any design choices is up to the user within the CSS area below.
  13. //I did however just fix a minor bug with the loop button going over top of search bar. Was a bitch to do and I had to add in another mutationobserver because fuck how CSS z-index works. Apparently just making the searchbar have a higher z-index then the button wouldn't work.
  14.  
  15. //append some CSS to the header with the += hack. You can write it like CSS and HTML between ``
  16. document.querySelector('head').insertAdjacentHTML('beforeend', `<style>
  17. .loopBtns{
  18. position: fixed;
  19. z-index: 5;
  20. top: 20px;
  21. left: 130px;
  22. font-weight: 900;
  23. color: silver;
  24. }
  25. </style>`
  26. );
  27.  
  28. //declare some variables and create some elements
  29. let loopBtn = document.createElement("BUTTON");
  30. let unLoopBtn = document.createElement("BUTTON");
  31. let appendTo = document.querySelector('body'); //Whatever you want to append to. I had to change it to body because the entire SPA kept re-rendering after every miniscule change. That's why I just made it fixed overtop the navbar. Thanks frameworks.
  32. let url = location.href;
  33.  
  34. //detect if entry point is NOT m.youtube.com/watch
  35. if(window.location.href.split('/')[3].substring(0, 5) !== "watch"){
  36. unLoopBtn.style.display = "none";
  37. loopBtn.style.display = "none";
  38. }
  39.  
  40.  
  41. loopBtn.innerHTML = "Loop";
  42. unLoopBtn.innerHTML = "Unloop";
  43. loopBtn.className = "loopBtns";
  44. unLoopBtn.className = "loopBtns";
  45.  
  46.  
  47. //make unloop class hidden, we'll unhide it and hide loop after loop is pressed and visa versa
  48. unLoopBtn.style.display = "none";
  49.  
  50. appendTo.appendChild(loopBtn);
  51. appendTo.appendChild(unLoopBtn);
  52.  
  53. //add some event listeners to make the video loop, hide the loop button/unloop button
  54. loopBtn.addEventListener("click", () => {
  55. document.querySelector('.html5-main-video').loop = true; //I originally had a video variable and was doing video.loop, but it was causing problems if you entered through anything other than the /watch url because there wouldn't be a video to assign to that variable. So don't change this querySelector.
  56. unLoopBtn.style.display = "block";
  57. loopBtn.style.display = "none";
  58. });
  59.  
  60. unLoopBtn.addEventListener("click", () => {
  61. document.querySelector('.html5-main-video').loop = false;
  62. unLoopBtn.style.display = "none";
  63. loopBtn.style.display = "block"
  64. });
  65.  
  66. //last, listen for the video source to change and change loop button to it's off state. I practically copied this from Mozilla's mutationObserver page and changed it to only look for if the video source changes.
  67.  
  68. let loopStateObserver = () => {
  69.  
  70. // Select the node that will be observed for mutations
  71. const targetNode = document.querySelector('#player');
  72.  
  73. // Options for the observer (which mutations to observe)
  74. const config = { attributes: true, childList: true, subtree: true };
  75.  
  76. // Callback function to execute when mutations are observed
  77. const callback = function(mutationsList, observer) {
  78. // don't worry about Internet Explorer, this is for youtube mobile and AFAIK there's no IE phone app. That's why I don't use traditional for loop here and I use arrow functions. What are the chances someone is actually going to be watching mobile youtube on their desktop in an ancient version of internet explorer anyways? At this point fuck devving for everyone, update your browser boomer.
  79. for(const mutation of mutationsList) {
  80. if (mutation.type === 'attributes' && mutation.attributeName === "src") {
  81. console.log('video source changed; checking if url changed');
  82. //check if URL also changed
  83. if(url!==location.href){
  84. document.querySelector('.html5-main-video').loop = false;
  85. unLoopBtn.style.display = "none";
  86. loopBtn.style.display = "block";
  87. url = location.href;
  88. console.log('updating loop button')
  89. }
  90. //this is the finishing touch. Just makes it so the button only shows up on the m.youtube.com/watch URL
  91. if(window.location.href.split('/')[3].substring(0, 5) !== "watch"){
  92. unLoopBtn.style.display = "none";
  93. loopBtn.style.display = "none";
  94. }
  95. }
  96. }
  97. };
  98.  
  99. // Create an observer instance linked to the callback function
  100. const observer = new MutationObserver(callback);
  101.  
  102. // Start observing the target node for configured mutations
  103. observer.observe(targetNode, config);
  104. }
  105.  
  106. //I know I said lastly already, but I also want to lower the z-index if searchbar comes up so the loop button doesn't go overtop the searchbar. So I'm gonna throw in another observer
  107. let searchStateObserver = () => {
  108. // Select the node that will be observed for mutations
  109. const targetNode = document.querySelector('ytm-mobile-topbar-renderer')
  110.  
  111. // Options for the observer (which mutations to observe)
  112. const config = { attributes: true, childList: true, subtree: true };
  113.  
  114. // Callback function to execute when mutations are observed
  115. const callback = function(mutationsList, observer) {
  116. // Use traditional 'for loops' for IE 11
  117. for(const mutation of mutationsList) {
  118. if(mutation.attributeName == "data-mode"){
  119. //don't use && in the top if statement, because I want to add in an else that won't trigger otherwise after the next if statement.
  120. if(document.querySelector('.mobile-topbar-header').dataset.mode === "searching"){
  121. console.log('search mode open');
  122. unLoopBtn.style.zIndex = 2;
  123. loopBtn.style.zIndex = 2;
  124. } else{
  125. console.log('search mode close');
  126. unLoopBtn.style.zIndex = 5;
  127. loopBtn.style.zIndex = 5;
  128. }
  129. }
  130. }
  131. };
  132. // Create an observer instance linked to the callback function
  133. const observer = new MutationObserver(callback);
  134. // Start observing the target node for configured mutations
  135. observer.observe(targetNode, config);
  136. }
  137.  
  138. //Call the two observer functions. I only wrapped them in functions to keep it clean and reuse the variables I copy pasted from MDN's mutationObserver page without using retarded names like callback2 or config2
  139. loopStateObserver();
  140. searchStateObserver();
  141.