Youtube Player Speed Slider

Add Speed Slider to Youtube Player Settings

当前为 2023-05-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Youtube Player Speed Slider
  3. // @namespace youtube_player_speed_slider
  4. // @version 0.4.0
  5. // @description Add Speed Slider to Youtube Player Settings
  6. // @author Łukasz
  7. // @match https://*.youtube.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. const YTS_BUILD_TIMEOUT = 500;
  12. const YTS_REMOVE_TIMEOUT = 1000;
  13.  
  14. const el = {
  15. menu: null,
  16. icon: null,
  17. label: null,
  18. check: null,
  19. slider: null,
  20. player: null,
  21. }
  22.  
  23.  
  24. let yts_event_em_speed = false;
  25. let yts_event_player = false;
  26.  
  27. const yts_r = 'yts_r';
  28. const yts_s = 'yts_s';
  29.  
  30.  
  31. /*************************************
  32. * INIT *
  33. ************************************/
  34. function ytsInit() {
  35. $yts.event(document, "spfdone", function () {
  36. ytsInitPlayer();
  37. });
  38. ytsReopenMenu();
  39. ytsBuildApp();
  40. }
  41.  
  42. function ytsBuildApp() {
  43.  
  44. el.menu = $yts.get('.ytp-panel-menu');
  45. console.log(el.menu);
  46. if (el.menu !== null) {
  47. setTimeout(ytsRemoveDefaultSpeed, YTS_REMOVE_TIMEOUT);
  48. ytsInitSlider();
  49. ytsInitMenu();
  50. ytsInitPlayer();
  51. } else {
  52. setTimeout(ytsBuildApp, YTS_BUILD_TIMEOUT);
  53. }
  54. }
  55.  
  56.  
  57. /*************************************
  58. * MENU *
  59. ************************************/
  60.  
  61. function ytsInitMenu() {
  62. el.icon.innerHTML = '<svg height="24" viewBox="0 0 24 24" width="24"><path d="M10,8v8l6-4L10,8L10,8z M6.3,5L5.7,4.2C7.2,3,9,2.2,11,2l0.1,1C9.3,3.2,7.7,3.9,6.3,5z M5,6.3L4.2,5.7C3,7.2,2.2,9,2,11 l1,.1C3.2,9.3,3.9,7.7,5,6.3z M5,17.7c-1.1-1.4-1.8-3.1-2-4.8L2,13c0.2,2,1,3.8,2.2,5.4L5,17.7z M11.1,21c-1.8-0.2-3.4-0.9-4.8-2 l-0.6,.8C7.2,21,9,21.8,11,22L11.1,21z M22,12c0-5.2-3.9-9.4-9-10l-0.1,1c4.6,.5,8.1,4.3,8.1,9s-3.5,8.5-8.1,9l0.1,1 C18.2,21.5,22,17.2,22,12z" fill="white"></path></svg>'
  63. const speedMenu = $yts.new('div', {'className': 'ytp-menuitem', id: 'yts-menu'});
  64. const right = $yts.new('div', {'className': 'ytp-menuitem-content'});
  65. right.appendChild(el.check);
  66. right.appendChild(el.slider);
  67. speedMenu.appendChild(el.icon);
  68. speedMenu.appendChild(el.label);
  69. speedMenu.appendChild(right);
  70. el.menu.appendChild(speedMenu);
  71. }
  72.  
  73. function ytsRemoveDefaultSpeed() {
  74. const switchers = $yts.getOpt(".ytp-menuitem", {role: 'menuitemcheckbox'});
  75. let toRemove = null;
  76.  
  77. if (!ytsPlayerHasClass('ad-interrupting') && switchers && switchers.length && !yts_event_em_speed) {
  78. toRemove = switchers[switchers.length - 1].nextElementSibling;
  79. if (toRemove && toRemove.id !== 'yts-menu') {
  80. $yts.style(toRemove, 'display', 'none');
  81. yts_event_em_speed = true;
  82. }
  83. }
  84. }
  85.  
  86. function ytsReopenMenu() {
  87. const settings_button = $yts.get(".ytp-settings-button");
  88. settings_button && settings_button.click();
  89. settings_button && settings_button.click();
  90. }
  91.  
  92.  
  93. /*************************************
  94. * SLIDER *
  95. ************************************/
  96.  
  97. function ytsInitSlider() {
  98. const rem = ytsParam(yts_r);
  99. let speed = ytsParam(yts_s) || 1;
  100. speed = rem ? speed : 1;
  101.  
  102. el.icon = $yts.new('div', {'className': 'ytp-menuitem-icon'});
  103. el.label = $yts.new('div', {'className': 'ytp-menuitem-label'});
  104. el.check = $yts.new('input', {
  105. 'type': 'checkbox',
  106. 'title': 'Remember speed',
  107. style: {
  108. 'accent-color': '#f00',
  109. 'width': '20px',
  110. 'height': '20px',
  111. 'margin': '0',
  112. 'padding': '0'
  113. }
  114. });
  115. el.slider = $yts.new('input', {
  116. 'type': 'range',
  117. 'min': 0.5,
  118. 'max': 4,
  119. 'step': 0.1,
  120. 'value': speed,
  121. style: {
  122. 'accent-color': '#f00',
  123. 'width': 'calc(100% - 30px)',
  124. 'margin': '0 5px',
  125. 'padding': '0'
  126. }
  127. });
  128. if (rem) {
  129. el.check.checked = true;
  130. }
  131. $yts.event(el.slider, 'change', ytsChangeSlider);
  132. $yts.event(el.check, 'change', ytsChangeRemember);
  133. $yts.event(el.slider, 'input', ytsChangeSlider);
  134. $yts.event(el.slider, 'wheel', ytsWheelSlider);
  135.  
  136. ytsUpdateSliderLabel(speed);
  137. }
  138.  
  139.  
  140. function ytsWheelSlider(event) {
  141. let val = parseFloat(event.target.value) + (event.wheelDelta > 0 ? 0.1 : -0.1);
  142. val = val < 0.5 ? 0.5 : (val > 4 ? 4 : val);
  143. if (event.target.value !== val) {
  144. event.target.value = val;
  145. ytsUpdateSliderLabel(val);
  146. }
  147. event.preventDefault();
  148. }
  149.  
  150. function ytsChangeSlider(event) {
  151. ytsUpdateSliderLabel(event.target.value);
  152. }
  153.  
  154. function ytsChangeRemember() {
  155. ytsParam(yts_r, ytsParam(yts_r) ? 0 : 1);
  156. }
  157.  
  158.  
  159. function ytsUpdateSliderLabel(val) {
  160. ytsSetPlayerDuration(val);
  161. el.label.innerHTML = 'Speed: ' + parseFloat(val).toFixed(1);
  162. }
  163.  
  164.  
  165. /*************************************
  166. * PLAYER *
  167. ************************************/
  168.  
  169. function ytsInitPlayer() {
  170. el.player = $yts.get('.html5-main-video');
  171. ytsObservePlayer();
  172. if (ytsParam(yts_s) && ytsParam(yts_r)) {
  173. ytsSetPlayerDuration(ytsParam(yts_s));
  174. ytsUpdateSliderLabel(ytsParam(yts_s));
  175. }
  176.  
  177. }
  178.  
  179. function ytsPlayerHasClass(cls) {
  180. ytsInitPlayer();
  181. return el.player && el.player.classList.contains(cls)
  182. }
  183.  
  184. function ytsSetPlayerDuration(value) {
  185. ytsParam(yts_s, value);
  186. if (el.player) {
  187. el.player.playbackRate = value;
  188. }
  189. }
  190.  
  191. function ytsObservePlayer() {
  192. if (!yts_event_player) {
  193. ytsObserver(el.player.parentNode.parentNode, function (mutation) {
  194. if (/html5-video-player/.test(mutation.target.className) && !yts_event_em_speed) {
  195. ytsRemoveDefaultSpeed();
  196. }
  197. });
  198. yts_event_player = true;
  199. }
  200. }
  201.  
  202.  
  203. /************************************
  204. * DOM *
  205. ************************************/
  206. $yts = {
  207. 'event': function (obj, event, callback) {
  208. obj.addEventListener(event, callback);
  209. },
  210. 'new': function (tag, option) {
  211. const element = document.createElement(tag);
  212. for (let param in option) {
  213. if (param === 'data' || param === 'style' || param === 'attr') {
  214. for (let data in option[param]) {
  215. $yts[param](element, data, option[param][data]);
  216. }
  217. } else {
  218. element[param] = option[param];
  219. }
  220. }
  221. return element;
  222. },
  223. 'get': function (tselector, all) {
  224. all = all || false;
  225. const type = tselector.substring(0, 1);
  226. const selector = tselector.substring(1);
  227. let elements;
  228. if (type === "#") {
  229. return document.getElementById(selector);
  230. } else if (type === ".") {
  231. elements = document.getElementsByClassName(selector);
  232. } else {
  233. elements = document.querySelectorAll(tselector);
  234. }
  235.  
  236. if (all) {
  237. return elements;
  238. } else {
  239. return elements.length ? elements[0] : null;
  240. }
  241. },
  242. 'data': function (elem, key, val) {
  243. key = key.replace(/-(\w)/gi, function (x) {
  244. return x.charAt(1).toUpperCase()
  245. });
  246. if (typeof val !== 'undefined') {
  247. elem.dataset[key] = val;
  248. }
  249. return elem.dataset[key];
  250.  
  251. },
  252. 'style': function (elem, key, val, priority) {
  253. priority = priority || '';
  254. if (typeof val !== 'undefined') {
  255. elem.style.setProperty(key, val, priority);
  256. }
  257. return elem.style.getPropertyValue(key);
  258.  
  259. },
  260. 'attr': function (elem, key, val) {
  261. if (typeof val !== 'undefined') {
  262. elem.setAttribute(key, val);
  263. }
  264. return elem.getAttribute(key);
  265.  
  266. },
  267. 'getOpt': function (selector, option) {
  268. const el = $yts.get(selector, true);
  269. const pass = [];
  270. let correct;
  271. for (let i = 0; i < el.length; i++) {
  272. correct = true;
  273. for (let prop in option) {
  274. if (!$yts.has(el[i], prop, option[prop])) {
  275. correct = false;
  276. break;
  277. }
  278. }
  279. if (correct) {
  280. pass.push(el[i]);
  281. }
  282. }
  283. return pass;
  284. },
  285. 'has': function (elem, key, val) {
  286. if (elem.hasAttribute(key)) {
  287. const attr = elem.getAttribute(key);
  288. if (val !== null) {
  289. return attr === val;
  290. }
  291. return true;
  292. }
  293. return false;
  294. }
  295. };
  296.  
  297. /*************************************
  298. * OBSERVER *
  299. ************************************/
  300. function ytsObserver(element, callback) {
  301. const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  302. if (MutationObserver) {
  303. const obs = new MutationObserver(function (mutations) {
  304. callback(mutations[0]);
  305. });
  306.  
  307. obs.observe(element, {
  308. childList: true,
  309. subtree: true,
  310. attributes: true,
  311. characterData: true,
  312. attributeOldValue: true,
  313. characterDataOldValue: true
  314. });
  315. }
  316. }
  317.  
  318. function ytsParam(key, val) {
  319. if (typeof val !== 'undefined') {
  320. localStorage.setItem(key, val);
  321. }
  322. return parseFloat(localStorage.getItem(key));
  323. }
  324.  
  325.  
  326. ytsInit();