Improved Speed Slider For YouTube

Add Speed Slider to Youtube Player Settings

  1. // ==UserScript==
  2. // @name Improved Speed Slider For YouTube
  3. // @namespace improved_youtube_player_speed_slider
  4. // @version 1.0
  5. // @description Add Speed Slider to Youtube Player Settings
  6. // @author Łukasz, pabli
  7. // @match https://*.youtube.com/*
  8. // @grant none
  9. // ==/UserScript==
  10. var yts_build_timeout = 500;
  11. var yts_remove_timeout = 1000;
  12.  
  13. var yts_el_menu = null;
  14. var yts_el_slider_label = null;
  15. var yts_el_slider_icon = null;
  16. var yts_el_slider_check = null;
  17. var yts_el_slider = null;
  18. var yts_el_player = null;
  19.  
  20. var yts_event_em_speed = false;
  21. var yts_event_player = false;
  22.  
  23. var yts_r = 'yts_r';
  24. var yts_s = 'yts_s';
  25.  
  26.  
  27. /*************************************
  28. * INIT *
  29. ************************************/
  30. function ytsInit() {
  31. $yts.event(document, "spfdone", function () {
  32. ytsInitPlayer();
  33. });
  34. ytsReopenMenu();
  35. ytsBuildApp();
  36. }
  37.  
  38. function ytsBuildApp() {
  39.  
  40. yts_el_menu = $yts.get('.ytp-panel-menu');
  41. if (yts_el_menu !== null) {
  42. setTimeout(ytsRemoveDefaultSpeed, yts_remove_timeout);
  43. ytsInitSlider();
  44. ytsInitMenu();
  45. ytsInitPlayer();
  46. }
  47. else {
  48. setTimeout(ytsBuildApp, yts_build_timeout);
  49. }
  50. }
  51.  
  52.  
  53. /*************************************
  54. * MENU *
  55. ************************************/
  56.  
  57. function ytsInitMenu() {
  58.  
  59. var speedMenu = $yts.new('div', {'className': 'ytp-menuitem', id: 'yts-menu'});
  60. var right = $yts.new('div', {'className': 'ytp-menuitem-content'});
  61. var cssMenu = $yts.new('style', {'type': 'text/css'});
  62. var css =`
  63. .ytp-menuitem-toggle-checkbox[type="checkbox"]{
  64. -webkit-appearance: none;
  65. -moz-appearance: none;
  66. appearance: none;
  67. outline: none;
  68. cursor: pointer;
  69. }
  70. .ytp-menuitem-toggle-checkbox:checked[type="checkbox"]
  71. {
  72. background: #f00;
  73. }
  74. .ytp-menuitem-toggle-checkbox:checked[type="checkbox"]:after
  75. {
  76. left: 20px;
  77. background-color: #fff;
  78. }
  79. .ytp-menuitem-slider {
  80. -webkit-appearance: none;
  81. vertical-align: middle;
  82. outline: none;
  83. border: none;
  84. padding: 0;
  85. background: none;
  86. }
  87. .ytp-menuitem-slider::-webkit-slider-runnable-track {
  88. background-color: rgba(255, 255, 255, .5);
  89. height: 6px;
  90. border-radius: 3px;
  91. border: 1px solid transparent;
  92. }
  93. .ytp-menuitem-slider[disabled]::-webkit-slider-runnable-track {
  94. border: 1px solid rgba(255, 255, 255, .5);
  95. background-color: transparent;
  96. opacity: 0.4;
  97. }
  98. .ytp-menuitem-slider::-moz-range-track {
  99. background-color: rgba(255, 255, 255, .5);
  100. height: 6px;
  101. border-radius: 3px;
  102. border: none;
  103. }
  104. .ytp-menuitem-slider::-ms-track {
  105. color: transparent;
  106. border: none;
  107. background: none;
  108. height: 6px;
  109. }
  110. .ytp-menuitem-slider::-ms-fill-lower {
  111. background-color: rgba(255, 255, 255, .5);
  112. border-radius: 3px;
  113. }
  114. .ytp-menuitem-slider::-ms-fill-upper {
  115. background-color: rgba(255, 255, 255, .5);
  116. border-radius: 3px;
  117. }
  118. .ytp-menuitem-slider::-ms-tooltip {
  119. display: none;
  120. }
  121. .ytp-menuitem-slider::-moz-range-thumb {
  122. border-radius: 20px;
  123. height: 14px;
  124. width: 14px;
  125. border: none;
  126. background: none;
  127. background-color: #fff;
  128. box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.6);
  129. -moz-transition: all .08s cubic-bezier(0.4, 0.0, 1, 1);
  130. }
  131. .ytp-menuitem-slider:active::-moz-range-thumb {
  132. outline: none;
  133. }
  134. .ytp-menuitem-slider::-webkit-slider-thumb {
  135. -webkit-appearance: none !important;
  136. border-radius: 100%;
  137. background-color: #fff;
  138. height: 14px;
  139. width: 14px;
  140. margin-top: -5px;
  141. box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.6);
  142. -moz-transition: all .08s cubic-bezier(0.4, 0.0, 1, 1);
  143. -webkit-transition: all .08s cubic-bezier(0.4, 0.0, 1, 1);
  144. }
  145. .ytp-menuitem-slider:active::-webkit-slider-thumb {
  146. outline: none;
  147. }
  148. .ytp-menuitem-slider::-ms-thumb {
  149. border-radius: 100%;
  150. background-color: #fff;
  151. height: 14px;
  152. width: 14px;
  153. border: none;
  154. }
  155. .ytp-menuitem-slider:active::-ms-thumb {
  156. border: none;
  157. }
  158. `;
  159. cssMenu.innerHTML = css;
  160. right.appendChild(yts_el_slider_check);
  161. right.appendChild(yts_el_slider);
  162. right.appendChild(cssMenu);
  163. speedMenu.appendChild(yts_el_slider_icon);
  164. speedMenu.appendChild(yts_el_slider_label);
  165. speedMenu.appendChild(right);
  166. yts_el_menu.appendChild(speedMenu);
  167. }
  168.  
  169. function ytsRemoveDefaultSpeed() {
  170. var switchers = $yts.getOpt(".ytp-menuitem", {role: 'menuitemcheckbox'});
  171. var toRemove = null;
  172.  
  173. if (!ytsPlayerHasClass('ad-interrupting') && switchers && switchers.length && !yts_event_em_speed) {
  174. toRemove = switchers[switchers.length - 1].nextElementSibling;
  175. if (toRemove && toRemove.id !== 'yts-menu') {
  176. $yts.style(toRemove, 'display', 'none');
  177. yts_event_em_speed = true;
  178. }
  179. }
  180. }
  181.  
  182. function ytsReopenMenu() {
  183. var settings_button = $yts.get(".ytp-settings-button");
  184. settings_button && settings_button.click();
  185. settings_button && settings_button.click();
  186. }
  187.  
  188.  
  189. /*************************************
  190. * SLIDER *
  191. ************************************/
  192.  
  193. function ytsInitSlider() {
  194. var rem = ytsParam(yts_r);
  195. var speed = ytsParam(yts_s) || 1;
  196. speed = rem ? speed : 1;
  197.  
  198. yts_el_slider_icon = $yts.new('div', {'className': 'ytp-menuitem-icon'});//pabli
  199. yts_el_slider_label = $yts.new('div', {'className': 'ytp-menuitem-label'});
  200. yts_el_slider_check = $yts.new('input', {
  201. 'type': 'checkbox',
  202. 'title': 'Remember speed',
  203. 'className': 'ytp-menuitem-toggle-checkbox'
  204. });
  205.  
  206. yts_el_slider = $yts.new('input', {
  207. 'className': 'ytp-menuitem-slider',
  208. 'type': 'range',
  209. 'min': 0.1,
  210. 'max': 5,
  211. 'step': 0.1,
  212. 'value': speed,
  213. style: {
  214. 'width': 'calc(100% - 60px)',
  215. 'margin': '0 5px',
  216. 'padding': '0',
  217. 'vertical-align': 'text-bottom'
  218. }
  219. });
  220. if(rem){
  221. yts_el_slider_check.checked = true;
  222. }
  223. $yts.event(yts_el_slider, 'change', ytsChangeSlider);
  224. $yts.event(yts_el_slider_check, 'change', ytsChangeRemember);
  225. $yts.event(yts_el_slider, 'input', ytsChangeSlider);
  226. $yts.event(yts_el_slider, 'wheel', ytsWheelSlider);
  227.  
  228. ytsUpdateSliderLabel(speed);
  229. }
  230.  
  231.  
  232. function ytsWheelSlider(event) {
  233. var val = parseFloat(event.target.value) + (event.wheelDelta > 0 ? 0.1 : -0.1);
  234. val = val < 0.1 ? 0.1 : (val > 5 ? 5 : val);
  235. if (event.target.value !== val) {
  236. event.target.value = val;
  237. ytsUpdateSliderLabel(val);
  238. }
  239. event.preventDefault();
  240. }
  241.  
  242. function ytsChangeSlider(event) {
  243. ytsUpdateSliderLabel(event.target.value);
  244. }
  245.  
  246. function ytsChangeRemember() {
  247. ytsParam(yts_r, ytsParam(yts_r) ? 0 : 1);
  248. }
  249.  
  250.  
  251. function ytsUpdateSliderLabel(val) {
  252. ytsSetPlayerDuration(val);
  253. yts_el_slider_label.innerHTML = 'Speed (' + parseFloat(val).toFixed(1) + ')';
  254. }
  255.  
  256.  
  257. /*************************************
  258. * PLAYER *
  259. ************************************/
  260.  
  261. function ytsInitPlayer() {
  262. yts_el_player = $yts.get('.html5-main-video');
  263. ytsObservePlayer();
  264. if (ytsParam(yts_s) && ytsParam(yts_r)) {
  265. ytsSetPlayerDuration(ytsParam(yts_s));
  266. ytsUpdateSliderLabel(ytsParam(yts_s));
  267. }
  268.  
  269. }
  270.  
  271. function ytsPlayerHasClass (cls) {
  272. ytsInitPlayer();
  273. return yts_el_player && yts_el_player.classList.contains(cls);
  274. }
  275.  
  276. function ytsSetPlayerDuration(value) {
  277. ytsParam(yts_s, value);
  278. if (yts_el_player) {
  279. yts_el_player.playbackRate = value;
  280. }
  281. }
  282.  
  283. function ytsObservePlayer() {
  284. if (!yts_event_player) {
  285. ytsObserver(yts_el_player.parentNode.parentNode, function (mutation) {
  286. if (/html5-video-player/.test(mutation.target.className) && !yts_event_em_speed) {
  287. ytsRemoveDefaultSpeed();
  288. }
  289. });
  290. yts_event_player = true;
  291. }
  292. }
  293.  
  294.  
  295. /************************************
  296. * DOM *
  297. ************************************/
  298. $yts = {
  299. 'event': function (obj, event, callback) {
  300. obj.addEventListener(event, callback);
  301. },
  302. 'new': function (tag, option) {
  303. var element = document.createElement(tag);
  304. for (var param in option) {
  305. if (param === 'data' || param === 'style' || param === 'attr') {
  306. for (var data in option[param]) {
  307. $yts[param](element, data, option[param][data]);
  308. }
  309. }
  310. else {
  311. element[param] = option[param];
  312. }
  313. }
  314. return element;
  315. },
  316. 'get': function (tselector, all) {
  317. all = all || false;
  318. var type = tselector.substring(0, 1);
  319. var selector = tselector.substring(1);
  320. var elements;
  321. if (type === "#") {
  322. return document.getElementById(selector);
  323. }
  324. else if (type === ".") {
  325. elements = document.getElementsByClassName(selector);
  326. }
  327. else {
  328. elements = document.querySelectorAll(tselector);
  329. }
  330.  
  331. if (all) {
  332. return elements;
  333. }
  334. else {
  335. return elements.length ? elements[0] : null;
  336. }
  337. },
  338. 'data': function (elem, key, val) {
  339. key = key.replace(/-(\w)/gi, function (x) {
  340. return x.charAt(1).toUpperCase();
  341. });
  342. if (typeof val !== 'undefined') {
  343. elem.dataset[key] = val;
  344. }
  345. return elem.dataset[key];
  346.  
  347. },
  348. 'style': function (elem, key, val, priority) {
  349. priority = priority || '';
  350. if (typeof val !== 'undefined') {
  351. elem.style.setProperty(key, val, priority);
  352. }
  353. return elem.style.getPropertyValue(key);
  354.  
  355. },
  356. 'attr': function (elem, key, val) {
  357. if (typeof val !== 'undefined') {
  358. elem.setAttribute(key, val);
  359. }
  360. return elem.getAttribute(key);
  361.  
  362. },
  363. 'getOpt': function (selector, option) {
  364. var el = $yts.get(selector, true);
  365. var pass = [];
  366. var correct;
  367. for (var i = 0; i < el.length; i++) {
  368. correct = true;
  369. for (var prop in option) {
  370. if (!$yts.has(el[i], prop, option[prop])) {
  371. correct = false;
  372. break;
  373. }
  374. }
  375. if (correct) {
  376. pass.push(el[i]);
  377. }
  378. }
  379. return pass;
  380. },
  381. 'has': function (elem, key, val) {
  382. if (elem.hasAttribute(key)) {
  383. var attr = elem.getAttribute(key);
  384. if (val !== null) {
  385. return attr == val;
  386. }
  387. return true;
  388. }
  389. return false;
  390. }
  391. };
  392.  
  393. /*************************************
  394. * OBSERVER *
  395. ************************************/
  396. function ytsObserver(element, callback) {
  397. var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  398. if (MutationObserver) {
  399. var obs = new MutationObserver(function (mutations) {
  400. callback(mutations[0]);
  401. });
  402.  
  403. obs.observe(element, {
  404. childList: true,
  405. subtree: true,
  406. attributes: true,
  407. characterData: true,
  408. attributeOldValue: true,
  409. characterDataOldValue: true
  410. });
  411. }
  412. }
  413.  
  414. function ytsParam(key, val) {
  415. if (typeof val !== 'undefined') {
  416. localStorage.setItem(key, val);
  417. }
  418. return parseFloat(localStorage.getItem(key));
  419. }
  420.  
  421. ytsInit();