YouTube Defaulter

Set speed, quality, subtitles and volume as default globally or specialize for each channel

当前为 2024-08-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Defaulter
  3. // @namespace https://greasyfork.org/ru/users/901750-gooseob
  4. // @version 1.10.2
  5. // @description Set speed, quality, subtitles and volume as default globally or specialize for each channel
  6. // @author GooseOb
  7. // @license MIT
  8. // @match https://www.youtube.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  10. // ==/UserScript==
  11.  
  12. (function(){// index.ts
  13. function debounce(callback, delay) {
  14. let timeout;
  15. return function(...args) {
  16. clearTimeout(timeout);
  17. timeout = window.setTimeout(() => {
  18. callback.apply(this, args);
  19. }, delay);
  20. };
  21. }
  22. var translations = {
  23. "be-BY": {
  24. OPEN_SETTINGS: "Адкрыць дадатковыя налады",
  25. SUBTITLES: "Субтытры",
  26. SPEED: "Хуткасьць",
  27. CUSTOM_SPEED: "Свая хуткасьць",
  28. CUSTOM_SPEED_HINT: 'Калі вызначана, будзе выкарыстоўвацца замест "хуткасьць"',
  29. QUALITY: "Якасьць",
  30. VOLUME: "Гучнасьць, %",
  31. GLOBAL: "глябальна",
  32. LOCAL: "гэты канал",
  33. SHORTS: "Адкрываць shorts як звычайныя",
  34. NEW_TAB: "Адкрываць відэа ў новай картцы",
  35. COPY_SUBS: "Капіяваць субтытры ў поўнаэкранным, Ctrl+C",
  36. STANDARD_MUSIC_SPEED: "Звычайная хуткасьць на каналах музыкаў",
  37. ENHANCED_BITRATE: "Палепшаны бітрэйт (для карыстальнікаў Premium)",
  38. SAVE: "Захаваць",
  39. EXPORT: "Экспарт",
  40. IMPORT: "Імпарт"
  41. }
  42. };
  43. var text = {
  44. OPEN_SETTINGS: "Open additional settings",
  45. SUBTITLES: "Subtitles",
  46. SPEED: "Speed",
  47. CUSTOM_SPEED: "Custom speed",
  48. CUSTOM_SPEED_HINT: 'If defined, will be used instead of "speed"',
  49. QUALITY: "Quality",
  50. VOLUME: "Volume, %",
  51. GLOBAL: "global",
  52. LOCAL: "this channel",
  53. SHORTS: "Open shorts as a usual video",
  54. NEW_TAB: "Open videos in a new tab",
  55. COPY_SUBS: "Copy subtitles by Ctrl+C in fullscreen mode",
  56. STANDARD_MUSIC_SPEED: "Normal speed on artist channels",
  57. ENHANCED_BITRATE: "Quality: Enhanced bitrate (for Premium users)",
  58. SAVE: "Save",
  59. DEFAULT: "-",
  60. EXPORT: "Export",
  61. IMPORT: "Import",
  62. ...translations[document.documentElement.lang]
  63. };
  64. var cfgLocalStorage = localStorage["YTDefaulter"];
  65. var cfg = cfgLocalStorage ? JSON.parse(cfgLocalStorage) : {
  66. _v: 4,
  67. global: {},
  68. channels: {},
  69. flags: {
  70. shortsToUsual: false,
  71. newTab: false,
  72. copySubs: false,
  73. standardMusicSpeed: false,
  74. enhancedBitrate: false
  75. }
  76. };
  77. var isDescendantOrTheSame = (child, parents) => {
  78. while (child !== null) {
  79. if (parents.includes(child))
  80. return true;
  81. child = child.parentNode;
  82. }
  83. return false;
  84. };
  85. var saveCfg = () => {
  86. const cfgCopy = { ...cfg };
  87. const channelsCfgCopy = { ...cfg.channels };
  88. outer:
  89. for (const key in channelsCfgCopy) {
  90. const channelCfg = channelsCfgCopy[key];
  91. if (channelCfg.subtitles)
  92. continue;
  93. for (const cfgKey in channelCfg)
  94. if (cfgKey !== "subtitles")
  95. continue outer;
  96. delete channelsCfgCopy[key];
  97. }
  98. cfgCopy.channels = channelsCfgCopy;
  99. localStorage["YTDefaulter"] = JSON.stringify(cfgCopy);
  100. };
  101. var updateValuesIn = (controls, cfgPart) => {
  102. controls["speed"].value = cfgPart["speed"] || text.DEFAULT;
  103. controls["customSpeed"].value = cfgPart["customSpeed"] || "";
  104. controls["quality"].value = cfgPart["quality"] || text.DEFAULT;
  105. controls["volume"].value = cfgPart["volume"] || "";
  106. controls["subtitles"].checked = cfgPart["subtitles"] || false;
  107. };
  108. var menuControls = {
  109. global: {
  110. ["speed"]: null,
  111. ["customSpeed"]: null,
  112. ["quality"]: null,
  113. ["volume"]: null,
  114. ["subtitles"]: null
  115. },
  116. thisChannel: {
  117. ["speed"]: null,
  118. ["customSpeed"]: null,
  119. ["quality"]: null,
  120. ["volume"]: null,
  121. ["subtitles"]: null
  122. },
  123. flags: {
  124. shortsToUsual: null,
  125. newTab: null,
  126. copySubs: null,
  127. standardMusicSpeed: null,
  128. enhancedBitrate: null
  129. },
  130. updateThisChannel() {
  131. updateValuesIn(this.thisChannel, channelConfig.current);
  132. },
  133. updateValues() {
  134. console.log(cfg.global, channelConfig.current, cfg.flags);
  135. console.log(this);
  136. updateValuesIn(this.global, cfg.global);
  137. this.updateThisChannel();
  138. for (const key in cfg.flags) {
  139. this.flags[key].checked = cfg.flags[key];
  140. }
  141. }
  142. };
  143. var updateCfg = () => {
  144. const doUpdate = cfg._v !== 4;
  145. if (doUpdate) {
  146. switch (cfg._v) {
  147. case 2:
  148. cfg.flags.standardMusicSpeed = false;
  149. cfg._v = 3;
  150. case 3:
  151. cfg.global.quality = cfg.global.qualityMax;
  152. delete cfg.global.qualityMax;
  153. for (const key in cfg.channels) {
  154. const currCfg = cfg.channels[key];
  155. currCfg.quality = currCfg.qualityMax;
  156. delete currCfg.qualityMax;
  157. }
  158. cfg._v = 4;
  159. }
  160. saveCfg();
  161. }
  162. return doUpdate;
  163. };
  164. updateCfg();
  165. var restoreFocusAfter = (cb) => {
  166. const el = document.activeElement;
  167. cb();
  168. el.focus();
  169. };
  170. var until = (getItem, check, msToWait = 1e4, msReqTimeout = 20) => new Promise((res, rej) => {
  171. const reqLimit = msToWait / msReqTimeout;
  172. let i = 0;
  173. const interval = setInterval(() => {
  174. if (i++ > reqLimit)
  175. exit(rej);
  176. const item = getItem();
  177. if (!check(item))
  178. return;
  179. exit(() => res(item));
  180. }, msReqTimeout);
  181. const exit = (cb) => {
  182. clearInterval(interval);
  183. cb();
  184. };
  185. });
  186. var untilAppear = (getItem, msToWait) => until(getItem, Boolean, msToWait);
  187. var ytSettingItems = {};
  188. var channelConfig = { current: null };
  189. var video;
  190. var subtitlesBtn;
  191. var muteBtn;
  192. var SPEED_NORMAL;
  193. var isSpeedChanged = false;
  194. var menu = {
  195. element: null,
  196. btn: null,
  197. isOpen: false,
  198. width: 0,
  199. _closeListener: {
  200. onClick(e) {
  201. const el = e.target;
  202. if (isDescendantOrTheSame(el, [menu.element, menu.btn]))
  203. return;
  204. menu.toggle();
  205. },
  206. onKeyUp(e) {
  207. if (e.code !== "Escape")
  208. return;
  209. menu._setOpen(false);
  210. menu.btn.focus();
  211. },
  212. add() {
  213. document.addEventListener("click", this.onClick);
  214. document.addEventListener("keyup", this.onKeyUp);
  215. },
  216. remove() {
  217. document.removeEventListener("click", this.onClick);
  218. document.removeEventListener("keyup", this.onKeyUp);
  219. }
  220. },
  221. firstElement: null,
  222. _setOpen(bool) {
  223. if (bool) {
  224. this.fixPosition();
  225. this.element.style.visibility = "visible";
  226. this._closeListener.add();
  227. this.firstElement.focus();
  228. } else {
  229. this.element.style.visibility = "hidden";
  230. this._closeListener.remove();
  231. }
  232. this.isOpen = bool;
  233. },
  234. toggle: debounce(function() {
  235. this._setOpen(!this.isOpen);
  236. }, 100),
  237. fixPosition() {
  238. const { y, height, width, left } = this.btn.getBoundingClientRect();
  239. this.element.style.top = y + height + 8 + "px";
  240. this.element.style.left = left + width - this.width + "px";
  241. }
  242. };
  243. var $ = (id) => document.getElementById(id);
  244. var getChannelUsername = (aboveTheFold) => /(?<=@|\/c\/).+?$/.exec(aboveTheFold.querySelector(".ytd-channel-name > a").href)?.[0];
  245. var getPlr = () => $("movie_player");
  246. var getAboveTheFold = () => $("above-the-fold");
  247. var getActionsBar = () => $("actions")?.querySelector("ytd-menu-renderer");
  248. var iconD = {
  249. ["quality"]: "M15,17h6v1h-6V17z M11,17H3v1h8v2h1v-2v-1v-2h-1V17z M14,8h1V6V5V3h-1v2H3v1h11V8z M18,5v1h3V5H18z M6,14h1v-2v-1V9H6v2H3v1 h3V14z M10,12h11v-1H10V12z",
  250. ["speed"]: "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"
  251. };
  252. var getYtElementFinder = (elems) => (name) => findInNodeList(elems, (el) => !!el.querySelector(`path[d="${iconD[name]}"]`));
  253. var untilChannelUsernameAppear = (aboveTheFold) => untilAppear(() => getChannelUsername(aboveTheFold)).catch(() => "");
  254. var isMusicChannel = (aboveTheFold) => !!aboveTheFold.querySelector(".badge-style-type-verified-artist");
  255. var findInNodeList = (list, callback) => {
  256. for (const item of list)
  257. if (callback(item))
  258. return item;
  259. };
  260. var ytMenu = {
  261. async updatePlayer(plr) {
  262. this.element = plr.querySelector(".ytp-settings-menu");
  263. this._btn = plr.querySelector(".ytp-settings-button");
  264. const clickBtn = this._btn.click.bind(this._btn);
  265. restoreFocusAfter(clickBtn);
  266. await delay(50);
  267. restoreFocusAfter(clickBtn);
  268. },
  269. element: null,
  270. _btn: null,
  271. isOpen() {
  272. return this.element.style.display !== "none";
  273. },
  274. setOpen(bool) {
  275. if (bool !== this.isOpen())
  276. this._btn.click();
  277. },
  278. openItem(item) {
  279. this.setOpen(true);
  280. item.click();
  281. return this.element.querySelectorAll(".ytp-panel-animate-forward .ytp-menuitem-label");
  282. },
  283. findInItem(item, callback) {
  284. return findInNodeList(this.openItem(item), callback);
  285. }
  286. };
  287. var validateVolume = (value) => {
  288. const num = +value;
  289. return num < 0 || num > 100 ? "out of range" : isNaN(num) ? "not a number" : false;
  290. };
  291. var getElCreator = (tag) => (props) => Object.assign(document.createElement(tag), props);
  292. var comparators = {
  293. ["quality"]: (target, current) => +target >= parseInt(current) && (cfg.flags.enhancedBitrate || !current.toLowerCase().includes("premium")),
  294. ["speed"]: (target, current) => target === current
  295. };
  296. var logger = {
  297. prefix: "[YT-Defaulter]",
  298. err(...msgs) {
  299. console.error(this.prefix, ...msgs);
  300. },
  301. outOfRange(what) {
  302. this.err(what, "value is out of range");
  303. }
  304. };
  305. var valueSetters = {
  306. _ytSettingItem(value, settingName) {
  307. const isOpen = ytMenu.isOpen();
  308. const compare = comparators[settingName];
  309. ytMenu.findInItem(ytSettingItems[settingName], (btn) => compare(value, btn.textContent))?.click();
  310. ytMenu.setOpen(isOpen);
  311. },
  312. speed(value) {
  313. this._ytSettingItem(isSpeedChanged ? SPEED_NORMAL : value, "speed");
  314. isSpeedChanged = !isSpeedChanged;
  315. },
  316. customSpeed(value) {
  317. try {
  318. video.playbackRate = isSpeedChanged ? 1 : +value;
  319. } catch {
  320. logger.outOfRange("Custom speed");
  321. return;
  322. }
  323. isSpeedChanged = !isSpeedChanged;
  324. },
  325. subtitles(value) {
  326. if (subtitlesBtn.ariaPressed !== value.toString())
  327. subtitlesBtn.click();
  328. },
  329. volume(value) {
  330. const num = +value;
  331. muteBtn ||= document.querySelector(".ytp-mute-button");
  332. const isMuted = muteBtn.dataset.titleNoTooltip !== "Mute";
  333. if (num === 0) {
  334. if (!isMuted)
  335. muteBtn.click();
  336. return;
  337. }
  338. if (isMuted)
  339. muteBtn.click();
  340. try {
  341. video.volume = num / 100;
  342. } catch {
  343. logger.outOfRange("Volume");
  344. }
  345. },
  346. quality(value) {
  347. this._ytSettingItem(value, "quality");
  348. }
  349. };
  350. var div = getElCreator("div");
  351. var input = getElCreator("input");
  352. var checkbox = (props) => input({ type: "checkbox", ...props });
  353. var option = getElCreator("option");
  354. var _label = getElCreator("label");
  355. var labelEl = (forId, props) => {
  356. const elem = _label(props);
  357. elem.setAttribute("for", forId);
  358. return elem;
  359. };
  360. var selectEl = getElCreator("select");
  361. var btnClass = "yt-spec-button-shape-next";
  362. var btnClassFocused = btnClass + "--focused";
  363. var _button = getElCreator("button");
  364. var button = (text2, props) => _button({
  365. textContent: text2,
  366. className: `${btnClass} ${btnClass}--tonal ${btnClass}--mono ${btnClass}--size-m`,
  367. onfocus() {
  368. this.classList.add(btnClassFocused);
  369. },
  370. onblur() {
  371. this.classList.remove(btnClassFocused);
  372. },
  373. ...props
  374. });
  375.  
  376. class Hint {
  377. constructor(prefix, props) {
  378. this.element = div(props);
  379. this.element.className ||= "YTDef-setting-hint";
  380. this.prefix = prefix;
  381. this.hide();
  382. }
  383. hide() {
  384. this.element.style.display = "none";
  385. }
  386. show(msg) {
  387. this.element.style.display = "block";
  388. if (msg)
  389. this.element.textContent = this.prefix + msg;
  390. }
  391. prefix;
  392. element;
  393. }
  394. var delay = (ms) => new Promise((res) => setTimeout(res, ms));
  395. var onPageChange = async () => {
  396. if (location.pathname !== "/watch")
  397. return;
  398. const aboveTheFold = await untilAppear(getAboveTheFold);
  399. const channelUsername = await untilChannelUsernameAppear(aboveTheFold);
  400. channelConfig.current = cfg.channels[channelUsername] ||= {};
  401. const plr = await untilAppear(getPlr);
  402. await delay(1000);
  403. const getAd = () => plr.querySelector(".ytp-ad-player-overlay");
  404. if (getAd())
  405. await until(getAd, (ad) => !ad, 200000);
  406. await ytMenu.updatePlayer(plr);
  407. const getMenuItems = () => ytMenu.element.querySelectorAll('.ytp-menuitem[role="menuitem"]');
  408. const getYtElement = getYtElementFinder(await until(getMenuItems, (arr) => !!arr.length));
  409. Object.assign(ytSettingItems, {
  410. quality: getYtElement("quality"),
  411. speed: getYtElement("speed")
  412. });
  413. if (!SPEED_NORMAL)
  414. restoreFocusAfter(() => {
  415. const btn = ytMenu.findInItem(ytSettingItems.speed, (btn2) => !+btn2.textContent);
  416. if (btn)
  417. SPEED_NORMAL = btn.textContent;
  418. });
  419. const doNotChangeSpeed = cfg.flags.standardMusicSpeed && isMusicChannel(aboveTheFold);
  420. const settings = {
  421. ...cfg.global,
  422. ...channelConfig.current
  423. };
  424. const isChannelSpeed = "speed" in channelConfig.current;
  425. const isChannelCustomSpeed = "customSpeed" in channelConfig.current;
  426. if (doNotChangeSpeed && !isChannelCustomSpeed || isChannelSpeed)
  427. delete settings.customSpeed;
  428. if (doNotChangeSpeed && !isChannelSpeed)
  429. settings.speed = SPEED_NORMAL;
  430. if (doNotChangeSpeed) {
  431. settings.speed = SPEED_NORMAL;
  432. delete settings.customSpeed;
  433. }
  434. const { customSpeed } = settings;
  435. delete settings.customSpeed;
  436. isSpeedChanged = false;
  437. video ||= plr.querySelector(".html5-main-video");
  438. subtitlesBtn ||= plr.querySelector(".ytp-subtitles-button");
  439. restoreFocusAfter(() => {
  440. for (const setting in settings)
  441. valueSetters[setting](settings[setting]);
  442. if (!isNaN(+customSpeed)) {
  443. isSpeedChanged = false;
  444. valueSetters.customSpeed(customSpeed);
  445. }
  446. ytMenu.setOpen(false);
  447. });
  448. if (menu.element) {
  449. menuControls.updateThisChannel();
  450. return;
  451. }
  452. menu.element = div({
  453. id: "YTDef-menu"
  454. });
  455. menu.btn = button("", {
  456. id: "YTDef-btn",
  457. ariaLabel: text.OPEN_SETTINGS,
  458. tabIndex: 0,
  459. onclick() {
  460. menu.toggle();
  461. }
  462. });
  463. const toOptions = (values, getText) => [
  464. option({
  465. value: text.DEFAULT,
  466. textContent: text.DEFAULT
  467. })
  468. ].concat(values.map((value) => option({
  469. value,
  470. textContent: getText(value)
  471. })));
  472. const speedValues = [
  473. "2",
  474. "1.75",
  475. "1.5",
  476. "1.25",
  477. SPEED_NORMAL,
  478. "0.75",
  479. "0.5",
  480. "0.25"
  481. ];
  482. const qualityValues = [
  483. "144",
  484. "240",
  485. "360",
  486. "480",
  487. "720",
  488. "1080",
  489. "1440",
  490. "2160",
  491. "4320"
  492. ];
  493. const createSection = (sectionId, title, sectionCfg) => {
  494. const section = div({ role: "group" });
  495. section.setAttribute("aria-labelledby", sectionId);
  496. const getLocalId = (name) => "YTDef-" + name + "-" + sectionId;
  497. const addItem = (name, innerHTML, elem) => {
  498. const item = div();
  499. const id = getLocalId(name);
  500. const label = labelEl(id, { innerHTML });
  501. const valueProp = elem.type === "checkbox" ? "checked" : "value";
  502. Object.assign(elem, {
  503. id,
  504. name,
  505. onchange() {
  506. const value = this[valueProp];
  507. if (value === "" || value === text.DEFAULT)
  508. delete sectionCfg[name];
  509. else
  510. sectionCfg[name] = value;
  511. }
  512. });
  513. const cfgValue = sectionCfg[name];
  514. if (cfgValue)
  515. setTimeout(() => {
  516. elem[valueProp] = cfgValue;
  517. });
  518. item.append(label, elem);
  519. section.append(item);
  520. menuControls[sectionId][name] = elem;
  521. if (elem.hint)
  522. section.append(elem.hint.element);
  523. return { elem };
  524. };
  525. const addSelectItem = (name, label, options, getText) => {
  526. const { elem } = addItem(name, label, selectEl({ value: text.DEFAULT }));
  527. elem.append(...toOptions(options, getText));
  528. return elem;
  529. };
  530. section.append(getElCreator("span")({ textContent: title, id: sectionId }));
  531. const firstElement = addSelectItem("speed", text.SPEED, speedValues, (val) => val);
  532. if (sectionId === "global")
  533. menu.firstElement = firstElement;
  534. addItem("customSpeed", text.CUSTOM_SPEED, input({
  535. type: "number",
  536. onfocus() {
  537. this.hint.show();
  538. },
  539. onblur() {
  540. this.hint.hide();
  541. },
  542. hint: new Hint("", { textContent: text.CUSTOM_SPEED_HINT })
  543. }));
  544. addSelectItem("quality", text.QUALITY, qualityValues, (val) => val + "p");
  545. addItem("volume", text.VOLUME, input({
  546. type: "number",
  547. min: "0",
  548. max: "100",
  549. oninput() {
  550. settings.volume = this.value;
  551. const warning = validateVolume(this.value);
  552. if (warning) {
  553. this.hint.show(warning);
  554. } else {
  555. this.hint.hide();
  556. }
  557. },
  558. hint: new Hint("Warning: ")
  559. }));
  560. addItem("subtitles", text.SUBTITLES, checkbox());
  561. return section;
  562. };
  563. const sections = div({ className: "YTDef-" + "sections" });
  564. sections.append(createSection("global", text.GLOBAL, cfg.global), createSection("thisChannel", text.LOCAL, channelConfig.current));
  565. const checkboxDiv = (id, prop, text2) => {
  566. const cont = div({ className: "check-cont" });
  567. id = "YTDef-" + id;
  568. const elem = checkbox({
  569. id,
  570. checked: cfg.flags[prop],
  571. onclick() {
  572. cfg.flags[prop] = this.checked;
  573. }
  574. });
  575. menuControls.flags[prop] = elem;
  576. cont.append(labelEl(id, { textContent: text2 }), elem);
  577. return cont;
  578. };
  579. const controlStatus = div();
  580. const updateControlStatus = (content) => {
  581. controlStatus.textContent = `[${new Date().toLocaleTimeString()}] ${content}`;
  582. };
  583. const controlDiv = div({ className: "control-cont" });
  584. controlDiv.append(button(text.SAVE, {
  585. onclick() {
  586. saveCfg();
  587. updateControlStatus(text.SAVE);
  588. }
  589. }), button(text.EXPORT, {
  590. onclick: () => {
  591. navigator.clipboard.writeText(localStorage["YTDefaulter"]).then(() => {
  592. updateControlStatus(text.EXPORT);
  593. });
  594. }
  595. }), button(text.IMPORT, {
  596. onclick: async () => {
  597. try {
  598. const raw = await navigator.clipboard.readText();
  599. const newCfg = JSON.parse(raw);
  600. if (typeof newCfg !== "object" || !newCfg._v) {
  601. throw new Error("Import: Invalid data");
  602. }
  603. if (!updateCfg()) {
  604. localStorage["YTDefaulter"] = raw;
  605. cfg = newCfg;
  606. }
  607. channelConfig.current = cfg.channels[channelUsername] ||= {};
  608. } catch (e) {
  609. updateControlStatus(e.message);
  610. return;
  611. }
  612. updateControlStatus(text.IMPORT);
  613. menuControls.updateValues();
  614. }
  615. }));
  616. menu.element.append(sections, checkboxDiv("shorts", "shortsToUsual", text.SHORTS), checkboxDiv("new-tab", "newTab", text.NEW_TAB), checkboxDiv("copy-subs", "copySubs", text.COPY_SUBS), checkboxDiv("standard-music-speed", "standardMusicSpeed", text.STANDARD_MUSIC_SPEED), checkboxDiv("enhanced-bitrate", "enhancedBitrate", text.ENHANCED_BITRATE), controlDiv, controlStatus);
  617. menu.element.addEventListener("keyup", (e) => {
  618. const el = e.target;
  619. if (e.code === "Enter" && el.type === "checkbox")
  620. el.checked = !el.checked;
  621. });
  622. const settingsIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  623. const iconStyle = {
  624. viewBox: "0 0 24 24",
  625. width: "24",
  626. height: "24",
  627. fill: "var(--yt-spec-text-primary)"
  628. };
  629. for (const key in iconStyle)
  630. settingsIcon.setAttribute(key, iconStyle[key]);
  631. settingsIcon.append($("settings"));
  632. menu.btn.setAttribute("aria-controls", "YTDef-menu");
  633. menu.btn.classList.add(btnClass + "--icon-button");
  634. menu.btn.append(settingsIcon);
  635. const actionsBar = await untilAppear(getActionsBar);
  636. actionsBar.insertBefore(menu.btn, actionsBar.lastChild);
  637. document.querySelector("ytd-popup-container").append(menu.element);
  638. menu.width = menu.element.getBoundingClientRect().width;
  639. sections.style.maxWidth = sections.offsetWidth + "px";
  640. };
  641. var lastHref;
  642. setInterval(() => {
  643. if (lastHref === location.href)
  644. return;
  645. lastHref = location.href;
  646. setTimeout(onPageChange, 1000);
  647. }, 1000);
  648. var onClick = (e) => {
  649. const { shortsToUsual, newTab } = cfg.flags;
  650. if (!shortsToUsual && !newTab)
  651. return;
  652. let el = e.target;
  653. if (el.tagName !== "A") {
  654. el = el.closest("a");
  655. if (!el)
  656. return;
  657. }
  658. if (!/shorts\/|watch\?v=/.test(el.href))
  659. return;
  660. if (shortsToUsual)
  661. el.href = el.href.replace("shorts/", "watch?v=");
  662. if (newTab) {
  663. el.target = "_blank";
  664. e.stopPropagation();
  665. }
  666. };
  667. document.addEventListener("click", onClick, { capture: true });
  668. document.addEventListener("keyup", (e) => {
  669. if (e.code === "Enter")
  670. return onClick(e);
  671. if (!e.ctrlKey || e.shiftKey)
  672. return;
  673. if (cfg.flags.copySubs && e.code === "KeyC") {
  674. const plr = document.querySelector(".html5-video-player");
  675. if (!plr?.classList.contains("ytp-fullscreen"))
  676. return;
  677. const text2 = Array.from(plr.querySelectorAll(".captions-text > span"), (line) => line.textContent).join(" ");
  678. navigator.clipboard.writeText(text2);
  679. return;
  680. }
  681. if (e.code !== "Space")
  682. return;
  683. e.stopPropagation();
  684. e.preventDefault();
  685. const customSpeedValue = channelConfig.current ? channelConfig.current.customSpeed || !channelConfig.current.speed && cfg.global.customSpeed : cfg.global.customSpeed;
  686. if (customSpeedValue)
  687. return valueSetters.customSpeed(customSpeedValue);
  688. restoreFocusAfter(() => {
  689. valueSetters["speed"]((channelConfig.current || cfg.global)["speed"]);
  690. });
  691. }, { capture: true });
  692. var listener = () => {
  693. if (menu.isOpen)
  694. menu.fixPosition();
  695. };
  696. window.addEventListener("scroll", listener);
  697. window.addEventListener("resize", listener);
  698. var m = "#" + "YTDef-menu";
  699. var d = " div";
  700. var i = " input";
  701. var s = " select";
  702. var bg = "var(--yt-spec-menu-background)";
  703. var underline = "border-bottom: 2px solid var(--yt-spec-text-primary);";
  704. document.head.append(getElCreator("style")({
  705. textContent: `
  706. #${"YTDef-btn"} {position: relative; margin-left: 8px}
  707. ${m} {
  708. display: flex;
  709. visibility: hidden;
  710. color: var(--yt-spec-text-primary);
  711. font-size: 14px;
  712. flex-direction: column;
  713. position: fixed;
  714. background: ${bg};
  715. border-radius: 2rem;
  716. padding: 1rem;
  717. text-align: center;
  718. box-shadow: 0px 4px 32px 0px var(--yt-spec-static-overlay-background-light);
  719. z-index: 2202
  720. }
  721. .control-cont > button {margin: .2rem}
  722. ${m + d} {display: flex; margin-bottom: 1rem}
  723. ${m + d + d} {
  724. flex-direction: column;
  725. margin: 0 2rem
  726. }
  727. ${m + d + d + d} {
  728. flex-direction: row;
  729. margin: 1rem 0
  730. }
  731. ${m + s}, ${m + i} {
  732. text-align: center;
  733. background: ${bg};
  734. border: none;
  735. ${underline}
  736. color: inherit;
  737. width: 5rem;
  738. padding: 0;
  739. margin-left: auto
  740. }
  741. ${m} .${"YTDef-setting-hint"} {margin: 0; text-align: end}
  742. ${m + i} {outline: none}
  743. ${m + d + d + d}:focus-within > label, ${m} .check-cont:focus-within > label {${underline}}
  744. ${m} .check-cont {padding: 0 1rem}
  745. ${m + s} {appearance: none; outline: none}
  746. ${m} label {margin-right: 1.5rem; white-space: nowrap}
  747. ${m + i}::-webkit-outer-spin-button,
  748. ${m + i}::-webkit-inner-spin-button {-webkit-appearance: none; margin: 0}
  749. ${m + i}[type=number] {-moz-appearance: textfield}
  750. ${m + s}::-ms-expand {display: none}`
  751. }));
  752. })()