Old Seek UI

Return the old YouTube video player seek UI, replacing the mobile-style ones they added circa 2021.

  1. // ==UserScript==
  2. // @name Old Seek UI
  3. // @version 0.2
  4. // @description Return the old YouTube video player seek UI, replacing the mobile-style ones they added circa 2021.
  5. // @author Taniko Yamamoto
  6. // @author https://github.com/YukisCoffee/yt-player-classicifier
  7. // @match https://www.youtube.com/*
  8. // @icon https://www.youtube.com/favicon.ico
  9. // @grant none
  10. // @run-at document-start
  11. // @namespace https://greasyfork.org/users/1132181
  12. // ==/UserScript==
  13.  
  14. (function() {
  15.  
  16. const ICONSET = {
  17. "back10": "M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z M 16.9,22 H 16 V 18.7 L 15,19 v -0.7 l 1.8,-0.6 h .1 V 22 z m 4.3,-1.8 c 0,.3 0,.6 -0.1,.8 l -0.3,.6 c 0,0 -0.3,.3 -0.5,.3 -0.2,0 -0.4,.1 -0.6,.1 -0.2,0 -0.4,0 -0.6,-0.1 -0.2,-0.1 -0.3,-0.2 -0.5,-0.3 -0.2,-0.1 -0.2,-0.3 -0.3,-0.6 -0.1,-0.3 -0.1,-0.5 -0.1,-0.8 v -0.7 c 0,-0.3 0,-0.6 .1,-0.8 l .3,-0.6 c 0,0 .3,-0.3 .5,-0.3 .2,0 .4,-0.1 .6,-0.1 .2,0 .4,0 .6,.1 .2,.1 .3,.2 .5,.3 .2,.1 .2,.3 .3,.6 .1,.3 .1,.5 .1,.8 v .7 z m -0.9,-0.8 v -0.5 c 0,0 -0.1,-0.2 -0.1,-0.3 0,-0.1 -0.1,-0.1 -0.2,-0.2 -0.1,-0.1 -0.2,-0.1 -0.3,-0.1 -0.1,0 -0.2,0 -0.3,.1 l -0.2,.2 c 0,0 -0.1,.2 -0.1,.3 v 2 c 0,0 .1,.2 .1,.3 0,.1 .1,.1 .2,.2 .1,.1 .2,.1 .3,.1 .1,0 .2,0 .3,-0.1 l .2,-0.2 c 0,0 .1,-0.2 .1,-0.3 v -1.5 z",
  18. "forward10": "m 10,19 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 h -2 c 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 0,-3.3 2.7,-6 6,-6 v 4 l 5,-5 -5,-5 v 4 c -4.4,0 -8,3.6 -8,8 z m 6.8,3 H 16 V 18.7 L 15,19 v -0.7 l 1.8,-0.6 h .1 V 22 z m 4.3,-1.8 c 0,.3 0,.6 -0.1,.8 l -0.3,.6 c 0,0 -0.3,.3 -0.5,.3 C 20,21.9 19.8,22 19.6,22 19.4,22 19.2,22 19,21.9 18.8,21.8 18.7,21.7 18.5,21.6 18.3,21.5 18.3,21.3 18.2,21 18.1,20.7 18.1,20.5 18.1,20.2 v -0.7 c 0,-0.3 0,-0.6 .1,-0.8 l .3,-0.6 c 0,0 .3,-0.3 .5,-0.3 .2,0 .4,-0.1 .6,-0.1 .2,0 .4,0 .6,.1 .2,.1 .3,.2 .5,.3 .2,.1 .2,.3 .3,.6 .1,.3 .1,.5 .1,.8 v .7 z m -0.8,-0.8 v -0.5 c 0,0 -0.1,-0.2 -0.1,-0.3 0,-0.1 -0.1,-0.1 -0.2,-0.2 -0.1,-0.1 -0.2,-0.1 -0.3,-0.1 -0.1,0 -0.2,0 -0.3,.1 l -0.2,.2 c 0,0 -0.1,.2 -0.1,.3 v 2 c 0,0 .1,.2 .1,.3 0,.1 .1,.1 .2,.2 .1,.1 .2,.1 .3,.1 .1,0 .2,0 .3,-0.1 l .2,-0.2 c 0,0 .1,-0.2 .1,-0.3 v -1.5 z",
  19. "back5": "M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z m -1.3,8.9 .2,-2.2 h 2.4 v .7 h -1.7 l -0.1,.9 c 0,0 .1,0 .1,-0.1 0,-0.1 .1,0 .1,-0.1 0,-0.1 .1,0 .2,0 h .2 c .2,0 .4,0 .5,.1 .1,.1 .3,.2 .4,.3 .1,.1 .2,.3 .3,.5 .1,.2 .1,.4 .1,.6 0,.2 0,.4 -0.1,.5 -0.1,.1 -0.1,.3 -0.3,.5 -0.2,.2 -0.3,.2 -0.4,.3 C 18.5,22 18.2,22 18,22 17.8,22 17.6,22 17.5,21.9 17.4,21.8 17.2,21.8 17,21.7 16.8,21.6 16.8,21.5 16.7,21.3 16.6,21.1 16.6,21 16.6,20.8 h .8 c 0,.2 .1,.3 .2,.4 .1,.1 .2,.1 .4,.1 .1,0 .2,0 .3,-0.1 L 18.5,21 c 0,0 .1,-0.2 .1,-0.3 v -0.6 l -0.1,-0.2 -0.2,-0.2 c 0,0 -0.2,-0.1 -0.3,-0.1 h -0.2 c 0,0 -0.1,0 -0.2,.1 -0.1,.1 -0.1,0 -0.1,.1 0,.1 -0.1,.1 -0.1,.1 h -0.7 z",
  20. "forward5": "m 10,19 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 h -2 c 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 0,-3.3 2.7,-6 6,-6 v 4 l 5,-5 -5,-5 v 4 c -4.4,0 -8,3.6 -8,8 z m 6.7,.9 .2,-2.2 h 2.4 v .7 h -1.7 l -0.1,.9 c 0,0 .1,0 .1,-0.1 0,-0.1 .1,0 .1,-0.1 0,-0.1 .1,0 .2,0 h .2 c .2,0 .4,0 .5,.1 .1,.1 .3,.2 .4,.3 .1,.1 .2,.3 .3,.5 .1,.2 .1,.4 .1,.6 0,.2 0,.4 -0.1,.5 -0.1,.1 -0.1,.3 -0.3,.5 -0.2,.2 -0.3,.2 -0.5,.3 C 18.3,22 18.1,22 17.9,22 17.7,22 17.5,22 17.4,21.9 17.3,21.8 17.1,21.8 16.9,21.7 16.7,21.6 16.7,21.5 16.6,21.3 16.5,21.1 16.5,21 16.5,20.8 h .8 c 0,.2 .1,.3 .2,.4 .1,.1 .2,.1 .4,.1 .1,0 .2,0 .3,-0.1 L 18.4,21 c 0,0 .1,-0.2 .1,-0.3 v -0.6 l -0.1,-0.2 -0.2,-0.2 c 0,0 -0.2,-0.1 -0.3,-0.1 h -0.2 c 0,0 -0.1,0 -0.2,.1 -0.1,.1 -0.1,0 -0.1,.1 0,.1 -0.1,.1 -0.1,.1 h -0.6 z",
  21. "backchapter": "m 16.436975,17.634454 c -0.573529,0 -1.191177,0.117647 -1.617647,0.441177 v 4.308938 c 0,0.191177 0.214706,0.132123 0.220588,0.132123 0.397059,-0.191176 0.970588,-0.323414 1.397059,-0.323414 0.57353,0 1.191177,0.117646 1.617647,0.441176 0.397059,-0.25 1.117648,-0.441176 1.617647,-0.441176 0.485295,0 0.985294,0.08846 1.397059,0.309053 0.120588,0.06177 0.220589,-0.05623 0.220589,-0.132698 v -4.294002 c -0.438235,-0.329412 -1.067647,-0.441177 -1.617648,-0.441177 -0.573529,0 -1.191176,0.117647 -1.617647,0.441177 -0.42647,-0.32353 -1.044117,-0.441177 -1.617647,-0.441177 z m 3.235294,0.588235 c 0.352942,0 0.705883,0.04411 1.029412,0.147059 v 3.382353 c -0.323529,-0.102941 -0.67647,-0.147059 -1.029412,-0.147059 -0.499999,0 -1.220588,0.191177 -1.617647,0.441177 v -3.382353 c 0.397059,-0.25 1.117648,-0.441177 1.617647,-0.441177 z m -0.674976,1.202322 v 1.303997 l 1.024241,-0.651999 z M 18,7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z",
  22. "forwardchapter": "m 16.436975,17.634454 c -0.573529,0 -1.191177,0.117647 -1.617647,0.441177 v 4.308938 c 0,0.191177 0.214706,0.132123 0.220588,0.132123 0.397059,-0.191176 0.970588,-0.323414 1.397059,-0.323414 0.57353,0 1.191177,0.117646 1.617647,0.441176 0.397059,-0.25 1.117648,-0.441176 1.617647,-0.441176 0.485295,0 0.985294,0.08846 1.397059,0.309053 0.120588,0.06177 0.220589,-0.05623 0.220589,-0.132698 v -4.294002 c -0.438235,-0.329412 -1.067647,-0.441177 -1.617648,-0.441177 -0.573529,0 -1.191176,0.117647 -1.617647,0.441177 -0.42647,-0.32353 -1.044117,-0.441177 -1.617647,-0.441177 z m 3.235294,0.588235 c 0.352942,0 0.705883,0.04411 1.029412,0.147059 v 3.382353 c -0.323529,-0.102941 -0.67647,-0.147059 -1.029412,-0.147059 -0.499999,0 -1.220588,0.191177 -1.617647,0.441177 v -3.382353 c 0.397059,-0.25 1.117648,-0.441177 1.617647,-0.441177 z m -0.674976,1.202322 v 1.303997 l 1.024241,-0.651999 z M 18,7 v 4 c -4.4,0 -8,3.6 -8,8 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 h -2 c 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 0,-3.3 2.7,-6 6,-6 v 4 l 5,-5 z"
  23. };
  24. const DEBUG = false;
  25.  
  26. // Player API reference
  27. var api;
  28. var bezel;
  29.  
  30. var animationStartTimer = 0;
  31. var animationShouldEndTimer = 0;
  32.  
  33. function log(a)
  34. {
  35. if (DEBUG) console.log(a);
  36. }
  37.  
  38. function getAnimationDuration(elm)
  39. {
  40. var prop = window.getComputedStyle(elm).animationDuration;
  41.  
  42. switch (true)
  43. {
  44. case "ms" == prop.substr(-2):
  45. return +prop.replace("ms", "");
  46. case "s" == prop.substr(-1):
  47. return +(prop.replace("s", "")) * 1000;
  48. }
  49. }
  50.  
  51. async function attemptEndAnimateBezel()
  52. {
  53. while (animationShouldEndTimer > Date.now())
  54. {
  55. await new Promise(r => requestAnimationFrame(r));
  56. }
  57.  
  58. animationStartTimer = 0;
  59. animationShouldEndTimer = 0;
  60. bezel.style.display = "none";
  61. }
  62.  
  63. function animateBezel()
  64. {
  65. var animationDuration = getAnimationDuration(bezel.querySelector(".ytp-bezel"));
  66.  
  67. bezel.style.display = "";
  68.  
  69. if (0 == animationShouldEndTimer)
  70. {
  71. animationStartTimer = Date.now();
  72. animationShouldEndTimer = animationStartTimer;
  73. }
  74.  
  75. animationShouldEndTimer += animationDuration;
  76.  
  77. attemptEndAnimateBezel();
  78. }
  79.  
  80. function waitToAnimate()
  81. {
  82. return new Promise(resolve => {
  83. setTimeout(() => {
  84. resolve();
  85. }, 10);
  86. });
  87. }
  88.  
  89. function createBezel(direction, duration, text = "", chapter = false)
  90. {
  91. return new Promise(resolve => {
  92. log("Creating bezel");
  93. var bezelElm = api.querySelector(".ytp-bezel");
  94. bezel = bezelElm.parentNode;
  95.  
  96. bezelElm.removeAttribute("aria-label");
  97.  
  98. // Get the icon from the iconset
  99. var icon;
  100. if (chapter)
  101. {
  102. icon = ICONSET[direction + "chapter"];
  103. }
  104. else if (ICONSET[direction + duration])
  105. {
  106. icon = ICONSET[direction + duration];
  107. }
  108. else
  109. {
  110. icon = "";
  111. }
  112.  
  113. var iconElm;
  114. if (iconElm = bezel.querySelector(".ytp-bezel-icon path"))
  115. {
  116. iconElm.setAttribute("d", icon);
  117. }
  118. else
  119. {
  120. bezel.querySelector(".ytp-bezel-icon").insertAdjacentHTML("beforeend",
  121. `<svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
  122. <path class="ytp-svg-fill" d="${icon}"></path>
  123. </svg>`
  124. );
  125. }
  126.  
  127. if ("" === text)
  128. {
  129. bezel.setAttribute("class", "ytp-bezel-text-hide");
  130. }
  131. else
  132. {
  133. bezel.setAttribute("class", "");
  134. bezel.querySelector(".ytp-bezel-text").innerText = text;
  135. }
  136.  
  137. bezel.style.display = "none";
  138.  
  139. resolve();
  140. });
  141. }
  142.  
  143. async function waitForElement(query, timeout = 500)
  144. {
  145. log("Waiting for element " + query + " with timeout in " + timeout + " ms.");
  146. var hasTimedOut = false;
  147.  
  148. setTimeout(function() {
  149. log("Wait for element " + query + " has timed out.");
  150. hasTimedOut = true;
  151. }, timeout);
  152.  
  153. while (null == document.querySelector(query) && !hasTimedOut)
  154. {
  155. await new Promise(r => requestAnimationFrame(r));
  156. }
  157.  
  158. var a;
  159. if (a = document.querySelector(query))
  160. {
  161. return a;
  162. }
  163. else
  164. {
  165. return null;
  166. }
  167. }
  168.  
  169. function handleSeekGui()
  170. {
  171. log("Handing seek GUI");
  172. var direction = this.dataset.side;
  173. var duration = this.querySelector(".ytp-doubletap-tooltip-label")
  174. .innerText.replace(/[\s|[A-Za-z]]*/g, "")
  175. ;
  176. var text = "";
  177. var isChapter = false;
  178.  
  179. if (this.classList.contains("ytp-chapter-seek"))
  180. {
  181. var textContainer = this.querySelector(".ytp-chapter-seek-text-legacy")
  182. text = textContainer.innerText;
  183. duration = 0;
  184. isChapter = true;
  185. }
  186.  
  187. createBezel(direction, duration, text, isChapter).then(waitToAnimate).then(animateBezel);
  188. }
  189.  
  190. async function attemptHookPlayer()
  191. {
  192. log("Attempting to hook player");
  193. var playerApi = await waitForElement(".html5-video-player", 5000);
  194.  
  195. if (playerApi)
  196. {
  197. log("Player API detected");
  198. api = playerApi;
  199.  
  200. var doubleTapElm = api.querySelector(".ytp-doubletap-ui-legacy") ?? api.querySelector(".ytp-doubletap-ui") ?? null;
  201.  
  202. if (doubleTapElm && !api.__oldSeekUi)
  203. {
  204. log("Doubletap detected: installing binding");
  205. (new MutationObserver(handleSeekGui.bind(doubleTapElm)))
  206. .observe(doubleTapElm, {"subtree": true, "childList": true, "characterData": "true"});
  207. api.__oldSeekUi = true;
  208. }
  209. }
  210. }
  211.  
  212. function insertContinuationEvent()
  213. {
  214. log("Inserting continuation events");
  215. if (window.ytspf && window.ytspf.enabled)
  216. {
  217. log("Inserted spf continuation events");
  218. document.addEventListener("spfdone", attemptHookPlayer);
  219. }
  220. else if (document.querySelector("ytd-app"))
  221. {
  222. log("Inserted kevlar continuation events");
  223. document.addEventListener("yt-page-data-updated", attemptHookPlayer);
  224. }
  225. }
  226.  
  227. function installInitialStyles()
  228. {
  229. log("Installed initial styles");
  230. document.head.insertAdjacentHTML("beforeend",
  231. `<style>
  232. .ytp-doubletap-ui, .ytp-doubletap-ui-legacy
  233. {
  234. display: none !important;
  235. }
  236. </style>`
  237. );
  238. }
  239.  
  240. function handleDOMContentLoaded()
  241. {
  242. log("domcontentloaded event fired");
  243. installInitialStyles();
  244. document.removeEventListener("DOMContentLoaded", handleDOMContentLoaded);
  245. }
  246.  
  247. async function main()
  248. {
  249. log("Old seek ui script loaded");
  250.  
  251. document.addEventListener("DOMContentLoaded", handleDOMContentLoaded);
  252.  
  253. // The player needs more time to init
  254. // So wait until a little while after page load to attempt
  255. // hooking the player
  256. window.addEventListener("load", function handleLoad() {
  257. log("load event fired");
  258. attemptHookPlayer();
  259. insertContinuationEvent();
  260. window.removeEventListener("load", handleLoad);
  261. });
  262. }
  263.  
  264. main();
  265.  
  266. })();