Youtube Player Speed Slider

Add Speed Slider to Youtube Player Settings

当前为 2024-11-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Youtube Player Speed Slider
  3. // @namespace youtube_player_speed_slider
  4. // @version 1.0.0
  5. // @description Add Speed Slider to Youtube Player Settings
  6. // @author Łukasz
  7. // @match https://*.youtube.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (() => {
  13. 'use strict';
  14. var _modules = {
  15. 'Checkbox.ts': (_unused_module, exports, _require) => {
  16. Object.defineProperty(exports, '__esModule', {value: true});
  17. exports.Checkbox = void 0;
  18. const Component_1 = _require('Component.ts');
  19. class Checkbox extends Component_1.default {
  20. constructor(checked) {
  21. super('input', {
  22. styles: {
  23. accentColor: '#f00',
  24. width: '20px',
  25. height: '20px',
  26. margin: '0',
  27. padding: '0',
  28. },
  29. attrs: {
  30. type: 'checkbox',
  31. title: 'Remember speed',
  32. checked: checked,
  33. },
  34. });
  35. }
  36. getValue() {
  37. return this.element.checked;
  38. }
  39. }
  40. exports.Checkbox = Checkbox;
  41. },
  42.  
  43. 'Component.ts': (_unused_module, exports, _require) => {
  44. Object.defineProperty(exports, '__esModule', {value: true});
  45. const Dom_1 = _require('Dom.ts');
  46. class Component {
  47. constructor(tag, props = {}) {
  48. this.element = Dom_1.Dom.create({tag, ...props});
  49. }
  50. addClassName(...className) {
  51. this.element.classList.add(...className);
  52. }
  53. event(event, callback) {
  54. this.element.addEventListener(event, callback);
  55. }
  56. getElement() {
  57. return this.element;
  58. }
  59. mount(parent) {
  60. parent.appendChild(this.element);
  61. }
  62. }
  63. exports['default'] = Component;
  64. },
  65.  
  66. 'Dom.ts': (_unused_module, exports) => {
  67. Object.defineProperty(exports, '__esModule', {value: true});
  68. exports.Dom = void 0;
  69. class Dom {
  70. static create(data) {
  71. const element = document.createElement(data.tag);
  72. if (typeof data.children === 'string') {
  73. element.innerHTML = data.children;
  74. } else if (data.children) {
  75. element.append(
  76. ...Dom.array(data.children).map((item) =>
  77. item instanceof HTMLElement ||
  78. item instanceof SVGElement
  79. ? item
  80. : Dom.create(item),
  81. ),
  82. );
  83. }
  84. Dom.applyClass(element, data.classes);
  85. Dom.applyAttrs(element, data.attrs);
  86. Dom.applyEvents(element, data.events);
  87. Dom.applyStyles(element, data.styles);
  88. return element;
  89. }
  90. static element(tag, classes, children) {
  91. return Dom.create({tag, classes, children});
  92. }
  93. static createSvg(data) {
  94. const element = document.createElementNS(
  95. 'http://www.w3.org/2000/svg',
  96. data.tag,
  97. );
  98. if (typeof data.children === 'string') {
  99. element.innerHTML = data.children;
  100. } else if (data.children) {
  101. element.append(
  102. ...Dom.array(data.children).map((item) =>
  103. item instanceof SVGElement
  104. ? item
  105. : Dom.createSvg(item),
  106. ),
  107. );
  108. }
  109. Dom.applyClass(element, data.classes);
  110. Dom.applyAttrs(element, data.attrs);
  111. Dom.applyEvents(element, data.events);
  112. Dom.applyStyles(element, data.styles);
  113. return element;
  114. }
  115. static array(element) {
  116. return Array.isArray(element) ? element : [element];
  117. }
  118. static elementSvg(tag, classes, children) {
  119. return Dom.createSvg({tag, classes, children});
  120. }
  121. static applyAttrs(element, attrs) {
  122. if (attrs) {
  123. Object.entries(attrs).forEach(([key, value]) => {
  124. if (value === undefined || value === false) {
  125. element.removeAttribute(key);
  126. } else {
  127. element.setAttribute(key, `${value}`);
  128. }
  129. });
  130. }
  131. }
  132. static applyStyles(element, styles) {
  133. if (styles) {
  134. Object.entries(styles).forEach(([key, value]) => {
  135. const name = key.replace(
  136. /[A-Z]/g,
  137. (c) => `-${c.toLowerCase()}`,
  138. );
  139. element.style.setProperty(name, value);
  140. });
  141. }
  142. }
  143. static applyEvents(element, events) {
  144. if (events) {
  145. Object.entries(events).forEach(([name, callback]) => {
  146. element.addEventListener(name, callback);
  147. });
  148. }
  149. }
  150. static applyClass(element, classes) {
  151. if (classes) {
  152. element.setAttribute('class', classes);
  153. }
  154. }
  155. }
  156. exports.Dom = Dom;
  157. },
  158.  
  159. 'Icon.ts': (_unused_module, exports, _require) => {
  160. Object.defineProperty(exports, '__esModule', {value: true});
  161. exports.Icon = void 0;
  162. const Component_1 = _require('Component.ts');
  163. const Dom_1 = _require('Dom.ts');
  164. const iconPath =
  165. 'M10.01,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';
  166. class Icon extends Component_1.default {
  167. constructor() {
  168. super('div', {
  169. classes: 'ytp-menuitem-icon',
  170. children: Dom_1.Dom.createSvg({
  171. tag: 'svg',
  172. attrs: {
  173. height: '24',
  174. width: '24',
  175. viewBox: '0 0 24 24',
  176. },
  177. children: Dom_1.Dom.createSvg({
  178. tag: 'path',
  179. attrs: {
  180. fill: 'white',
  181. d: iconPath,
  182. },
  183. }),
  184. }),
  185. });
  186. }
  187. }
  188. exports.Icon = Icon;
  189. },
  190.  
  191. 'Label.ts': (_unused_module, exports, _require) => {
  192. Object.defineProperty(exports, '__esModule', {value: true});
  193. exports.Label = void 0;
  194. const Component_1 = _require('Component.ts');
  195. class Label extends Component_1.default {
  196. constructor(speed, label = 'Speed') {
  197. super('div', {classes: 'ytp-menuitem-label'});
  198. this.speed = '1.0';
  199. this.label = label;
  200. this.updateSpeed(speed);
  201. }
  202. updateLabel(label = 'Speed') {
  203. this.label = label;
  204. this.updateText();
  205. }
  206. updateSpeed(speed) {
  207. this.speed = speed.toFixed(1);
  208. this.updateText();
  209. }
  210. updateText() {
  211. this.element.innerText = `${this.label}: ${this.speed}`;
  212. }
  213. }
  214. exports.Label = Label;
  215. },
  216.  
  217. 'Menu.ts': (_unused_module, exports, _require) => {
  218. Object.defineProperty(exports, '__esModule', {value: true});
  219. exports.Menu = void 0;
  220. const SpeedMenuItem_1 = _require('SpeedMenuItem.ts');
  221. const delay_1 = _require('delay.ts');
  222. class Menu {
  223. constructor() {
  224. this.getMenu();
  225. }
  226. getMenu() {
  227. return document.querySelector(
  228. '.ytp-settings-menu .ytp-panel-menu',
  229. );
  230. }
  231. getDefaultMenuItem() {
  232. const defaultSpeedItem = [
  233. ...document.querySelectorAll('.ytp-menuitem'),
  234. ].filter((e) => {
  235. var _a;
  236. const path =
  237. (_a = e.querySelector('.ytp-menuitem-icon path')) ===
  238. null || _a === void 0
  239. ? void 0
  240. : _a.getAttribute('d');
  241. return path === null || path === void 0
  242. ? void 0
  243. : path.startsWith('M10,8v8l6-4L10,');
  244. });
  245. if (defaultSpeedItem.length) {
  246. return defaultSpeedItem[0];
  247. }
  248. return undefined;
  249. }
  250. getLabel() {
  251. var _a;
  252. const label =
  253. (_a = this.getDefaultMenuItem()) === null || _a === void 0
  254. ? void 0
  255. : _a.querySelector('.ytp-menuitem-label');
  256. return label === null || label === void 0
  257. ? void 0
  258. : label.innerText;
  259. }
  260. async reopenMenu() {
  261. var _a, _b;
  262. const menuButton = document.querySelector(
  263. '.ytp-settings-button',
  264. );
  265. const menu = this.getMenu();
  266. if (menu && this.menuHasCustomItem(menu)) {
  267. return;
  268. }
  269. if (menuButton) {
  270. (_a =
  271. menu === null || menu === void 0
  272. ? void 0
  273. : menu.style) === null || _a === void 0
  274. ? void 0
  275. : _a.setProperty('opacity', '0');
  276. menuButton.click();
  277. await (0, delay_1.delay)(50);
  278. menuButton.click();
  279. (_b =
  280. menu === null || menu === void 0
  281. ? void 0
  282. : menu.style) === null || _b === void 0
  283. ? void 0
  284. : _b.setProperty('opacity', '1');
  285. await (0, delay_1.delay)(50);
  286. }
  287. }
  288. menuHasCustomItem(menu) {
  289. return Boolean(
  290. menu.querySelector(`#${SpeedMenuItem_1.SpeedMenuItem.ID}`),
  291. );
  292. }
  293. addCustomSpeedItem(item) {
  294. var _a;
  295. const menu = this.getMenu();
  296. const defaultItem = this.getDefaultMenuItem();
  297. if (menu === null) {
  298. return false;
  299. }
  300. if (this.menuHasCustomItem(menu)) {
  301. (_a =
  302. defaultItem === null || defaultItem === void 0
  303. ? void 0
  304. : defaultItem.parentNode) === null || _a === void 0
  305. ? void 0
  306. : _a.removeChild(defaultItem);
  307. return true;
  308. }
  309. if (defaultItem) {
  310. defaultItem.replaceWith(item.getElement());
  311. } else {
  312. menu.appendChild(item.getElement());
  313. }
  314. return true;
  315. }
  316. }
  317. exports.Menu = Menu;
  318. },
  319.  
  320. 'Player.ts': (_unused_module, exports) => {
  321. Object.defineProperty(exports, '__esModule', {value: true});
  322. exports.Player = void 0;
  323. class Player {
  324. constructor(speed) {
  325. this.speed = speed;
  326. this.player = null;
  327. this.setSpeed(this.speed);
  328. }
  329. getPlayer() {
  330. if (!this.player) {
  331. this.player = document.querySelector('.html5-main-video');
  332. if (this.player) {
  333. this.initEvent(this.player);
  334. }
  335. }
  336. return this.player;
  337. }
  338. initEvent(player) {
  339. if (!player.getAttribute(Player.READY_FLAG)) {
  340. player.addEventListener(
  341. 'ratechange',
  342. this.checkPlayerSpeed.bind(this),
  343. );
  344. player.setAttribute(Player.READY_FLAG, 'ready');
  345. }
  346. }
  347. checkPlayerSpeed() {
  348. const player = this.getPlayer();
  349. if (
  350. player &&
  351. Math.abs(player.playbackRate - this.speed) > 0.01
  352. ) {
  353. player.playbackRate = this.speed;
  354. setTimeout(this.checkPlayerSpeed.bind(this), 200);
  355. }
  356. }
  357. setSpeed(speed) {
  358. this.speed = speed;
  359. const player = this.getPlayer();
  360. if (player !== null) {
  361. player.playbackRate = speed;
  362. }
  363. }
  364. }
  365. exports.Player = Player;
  366. Player.READY_FLAG = 'yts-listener';
  367. },
  368.  
  369. 'Slider.ts': (_unused_module, exports, _require) => {
  370. Object.defineProperty(exports, '__esModule', {value: true});
  371. exports.Slider = void 0;
  372. const Component_1 = _require('Component.ts');
  373. class Slider extends Component_1.default {
  374. constructor(speed) {
  375. super('input', {
  376. attrs: {
  377. type: 'range',
  378. min: Slider.MIN_VALUE,
  379. max: Slider.MAX_VALUE,
  380. step: 0.05,
  381. value: speed.toString(),
  382. },
  383. styles: {
  384. accentColor: '#f00',
  385. width: 'calc(100% - 30px)',
  386. margin: '0 5px',
  387. padding: '0',
  388. },
  389. });
  390. }
  391. setSpeed(speed) {
  392. this.element.value = speed.toString();
  393. }
  394. getSpeed() {
  395. return parseFloat(this.element.value);
  396. }
  397. }
  398. exports.Slider = Slider;
  399. Slider.MIN_VALUE = 0.5;
  400. Slider.MAX_VALUE = 4;
  401. },
  402.  
  403. 'SpeedMenuItem.ts': (_unused_module, exports, _require) => {
  404. Object.defineProperty(exports, '__esModule', {value: true});
  405. exports.SpeedMenuItem = void 0;
  406. const Component_1 = _require('Component.ts');
  407. const Dom_1 = _require('Dom.ts');
  408. class SpeedMenuItem extends Component_1.default {
  409. constructor() {
  410. super('div', {
  411. classes: 'ytp-menuitem',
  412. attrs: {
  413. id: SpeedMenuItem.ID,
  414. },
  415. });
  416. this.wrapper = Dom_1.Dom.element('div', 'ytp-menuitem-content');
  417. }
  418. addElement(icon, label, slider, checkbox) {
  419. this.element.append(icon, label, this.wrapper);
  420. this.wrapper.append(checkbox, slider);
  421. }
  422. }
  423. exports.SpeedMenuItem = SpeedMenuItem;
  424. SpeedMenuItem.ID = 'yts-speed-menu-item';
  425. },
  426.  
  427. 'AppController.ts': (_unused_module, exports, _require) => {
  428. Object.defineProperty(exports, '__esModule', {value: true});
  429. exports.AppController = void 0;
  430. const Icon_1 = _require('Icon.ts');
  431. const Label_1 = _require('Label.ts');
  432. const Slider_1 = _require('Slider.ts');
  433. const Checkbox_1 = _require('Checkbox.ts');
  434. const Store_1 = _require('Store.ts');
  435. const SpeedMenuItem_1 = _require('SpeedMenuItem.ts');
  436. const Menu_1 = _require('Menu.ts');
  437. const Player_1 = _require('Player.ts');
  438. const Observer_1 = _require('Observer.ts');
  439. class AppController {
  440. constructor() {
  441. this.rememberSpeed = new Store_1.Store('yts-remember-speed');
  442. this.speed = new Store_1.Store('yts-speed');
  443. const initialSpeed = this.getSpeed();
  444. this.menu = new Menu_1.Menu();
  445. this.player = new Player_1.Player(initialSpeed);
  446. this.speedMenuItem = new SpeedMenuItem_1.SpeedMenuItem();
  447. this.icon = new Icon_1.Icon();
  448. this.label = new Label_1.Label(initialSpeed);
  449. this.slider = new Slider_1.Slider(initialSpeed);
  450. this.checkbox = new Checkbox_1.Checkbox(
  451. this.rememberSpeed.get(false),
  452. );
  453. this.observer = new Observer_1.Observer();
  454. this.speedMenuItem.addElement(
  455. this.icon.getElement(),
  456. this.label.getElement(),
  457. this.slider.getElement(),
  458. this.checkbox.getElement(),
  459. );
  460. this.initEvents();
  461. }
  462. initEvents() {
  463. this.slider.event('change', this.sliderChangeEvent.bind(this));
  464. this.slider.event('input', this.sliderChangeEvent.bind(this));
  465. this.slider.event('wheel', this.sliderWheelEvent.bind(this));
  466. this.checkbox.event('change', this.checkboxEvent.bind(this));
  467. document.addEventListener('spfdone', this.initApp.bind(this));
  468. }
  469. sliderChangeEvent(_) {
  470. this.updateSpeed(this.slider.getSpeed());
  471. }
  472. checkboxEvent(_) {
  473. this.rememberSpeed.set(this.checkbox.getValue());
  474. }
  475. sliderWheelEvent(event) {
  476. const current = this.slider.getSpeed();
  477. const diff = event.deltaY > 0 ? -0.05 : 0.05;
  478. const value = Math.max(
  479. Slider_1.Slider.MIN_VALUE,
  480. Math.min(current + diff, Slider_1.Slider.MAX_VALUE),
  481. );
  482. if (current != value) {
  483. this.slider.setSpeed(value);
  484. this.updateSpeed(value);
  485. }
  486. event.preventDefault();
  487. }
  488. updateSpeed(speed) {
  489. this.speed.set(speed);
  490. this.player.setSpeed(speed);
  491. this.label.updateSpeed(speed);
  492. }
  493. getSpeed() {
  494. return this.rememberSpeed.get() ? this.speed.get(1) : 1;
  495. }
  496. mutationCallback() {
  497. this.initApp();
  498. }
  499. async initApp() {
  500. this.player.setSpeed(this.getSpeed());
  501. await this.menu.reopenMenu();
  502. const label = this.menu.getLabel();
  503. if (label) {
  504. this.label.updateLabel(label);
  505. }
  506. const player = this.player.getPlayer();
  507. if (player) {
  508. this.observer.start(
  509. player,
  510. this.mutationCallback.bind(this),
  511. );
  512. }
  513. return this.menu.addCustomSpeedItem(this.speedMenuItem);
  514. }
  515. }
  516. exports.AppController = AppController;
  517. },
  518.  
  519. 'Observer.ts': (_unused_module, exports) => {
  520. Object.defineProperty(exports, '__esModule', {value: true});
  521. exports.Observer = void 0;
  522. class Observer {
  523. stop() {
  524. if (this.observer) {
  525. this.observer.disconnect();
  526. }
  527. }
  528. start(element, callback) {
  529. this.stop();
  530. this.observer = new MutationObserver(callback);
  531. this.observer.observe(element, {
  532. childList: true,
  533. subtree: true,
  534. attributes: true,
  535. characterData: true,
  536. attributeOldValue: true,
  537. characterDataOldValue: true,
  538. });
  539. }
  540. }
  541. exports.Observer = Observer;
  542. },
  543.  
  544. 'Store.ts': (_unused_module, exports) => {
  545. Object.defineProperty(exports, '__esModule', {value: true});
  546. exports.Store = void 0;
  547. class Store {
  548. constructor(key) {
  549. this.key = key;
  550. }
  551. encode(val) {
  552. return JSON.stringify(val);
  553. }
  554. decode(val) {
  555. return JSON.parse(val);
  556. }
  557. set(value) {
  558. try {
  559. localStorage.setItem(this.key, this.encode(value));
  560. } catch (e) {
  561. return;
  562. }
  563. }
  564. get(defaultValue = undefined) {
  565. try {
  566. const data = localStorage.getItem(this.key);
  567. if (data) {
  568. return this.decode(data);
  569. }
  570. return defaultValue;
  571. } catch (e) {
  572. return defaultValue;
  573. }
  574. }
  575. remove() {
  576. localStorage.removeItem(this.key);
  577. }
  578. }
  579. exports.Store = Store;
  580. },
  581.  
  582. 'delay.ts': (_unused_module, exports) => {
  583. Object.defineProperty(exports, '__esModule', {value: true});
  584. exports.delay = void 0;
  585. async function delay(ms = 1000) {
  586. return await new Promise((resolve) => {
  587. setTimeout(resolve, ms);
  588. });
  589. }
  590. exports.delay = delay;
  591. },
  592. };
  593.  
  594. var _module_cache = {};
  595.  
  596. function _require(moduleId) {
  597. var cachedModule = _module_cache[moduleId];
  598. if (cachedModule !== undefined) {
  599. return cachedModule.exports;
  600. }
  601.  
  602. var module = (_module_cache[moduleId] = {
  603. exports: {},
  604. });
  605.  
  606. _modules[moduleId](module, module.exports, _require);
  607.  
  608. return module.exports;
  609. }
  610.  
  611. var _exports = {};
  612.  
  613. (() => {
  614. var exports = _exports;
  615. var _unused_export;
  616.  
  617. _unused_export = {value: true};
  618. const AppController_1 = _require('AppController.ts');
  619. const app = new AppController_1.AppController();
  620. async function init() {
  621. const ok = await app.initApp();
  622. if (!ok) {
  623. window.setTimeout(init, 2000);
  624. }
  625. }
  626. document.addEventListener('spfdone', init);
  627. init();
  628. })();
  629. })();