YouTube Defaulter

Set default speed, quality, subtitles and volume globally or per channel

  1. // ==UserScript==
  2. // @name YouTube Defaulter
  3. // @namespace https://greasyfork.org/ru/users/901750-gooseob
  4. // @version 1.12.8
  5. // @description Set default speed, quality, subtitles and volume globally or per 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(){// src/config/update.ts
  13. var update = (cfg) => {
  14. let isUpdated = true;
  15. switch (cfg._v) {
  16. case 4:
  17. if ("shortsToUsual" in cfg.flags) {
  18. cfg.flags.shortsToRegular = cfg.flags.shortsToUsual;
  19. delete cfg.flags.shortsToUsual;
  20. } else {
  21. isUpdated = false;
  22. }
  23. break;
  24. case 2:
  25. cfg.flags.standardMusicSpeed = false;
  26. cfg._v = 3;
  27. case 3:
  28. cfg.global.quality = cfg.global.qualityMax;
  29. delete cfg.global.qualityMax;
  30. for (const key in cfg.channels) {
  31. const currCfg = cfg.channels[key];
  32. currCfg.quality = currCfg.qualityMax;
  33. delete currCfg.qualityMax;
  34. }
  35. cfg._v = 4;
  36. }
  37. return isUpdated;
  38. };
  39. // src/config/value.ts
  40. var cfgLocalStorage = localStorage.YTDefaulter;
  41. var value = cfgLocalStorage ? JSON.parse(cfgLocalStorage) : {
  42. _v: 4,
  43. global: {},
  44. channels: {},
  45. flags: {
  46. shortsToRegular: false,
  47. newTab: false,
  48. copySubs: false,
  49. standardMusicSpeed: false,
  50. enhancedBitrate: false
  51. }
  52. };
  53.  
  54. // src/config/save.ts
  55. var saveLS = (newCfg) => {
  56. saveLSRaw(JSON.stringify(newCfg));
  57. };
  58. var saveLSRaw = (raw) => {
  59. localStorage.YTDefaulter = raw;
  60. };
  61. var save = (raw) => {
  62. const newCfg = JSON.parse(raw);
  63. if (typeof newCfg !== "object" || !newCfg._v) {
  64. throw new Error("Invalid data");
  65. }
  66. if (update(newCfg)) {
  67. saveLS(newCfg);
  68. } else {
  69. saveLSRaw(raw);
  70. }
  71. Object.assign(value, newCfg);
  72. };
  73. // src/config/prune.ts
  74. var prune = () => {
  75. outer:
  76. for (const key in value.channels) {
  77. const channelCfg = value.channels[key];
  78. if (channelCfg.subtitles)
  79. continue;
  80. for (const cfgKey in channelCfg) {
  81. if (cfgKey !== "subtitles")
  82. continue outer;
  83. }
  84. delete value.channels[key];
  85. }
  86. };
  87. // src/utils/ref.ts
  88. var ref = (val) => ({ val });
  89.  
  90. // src/config/current-channel.ts
  91. var username = ref("");
  92. var channel = () => value.channels[username.val] ||= {};
  93. // src/listeners/click.ts
  94. var onClick = (e) => {
  95. const { shortsToRegular, newTab } = value.flags;
  96. if (shortsToRegular || newTab) {
  97. let el = e.target;
  98. if (el.tagName !== "A") {
  99. el = el.closest("a");
  100. }
  101. if (el) {
  102. const isShorts = el.href.includes("/shorts/");
  103. if (shortsToRegular && isShorts) {
  104. el.href = el.href.replace("shorts/", "watch?v=");
  105. }
  106. const isNormal = el.href.includes("/watch?v=");
  107. if (newTab && (isShorts || isNormal)) {
  108. el.target = "_blank";
  109. e.stopPropagation();
  110. }
  111. }
  112. }
  113. };
  114. // src/utils/$.ts
  115. var $ = (id) => document.getElementById(id);
  116. // src/utils/debounce.ts
  117. var debounce = (callback, delay) => {
  118. let timeout;
  119. return (...args) => {
  120. clearTimeout(timeout);
  121. timeout = window.setTimeout(() => {
  122. callback(...args);
  123. }, delay);
  124. };
  125. };
  126. // src/utils/is-descendant-or-the-same.ts
  127. var isDescendantOrTheSame = (child, parents) => {
  128. while (child !== null) {
  129. if (parents.includes(child))
  130. return true;
  131. child = child.parentNode;
  132. }
  133. return false;
  134. };
  135. // src/utils/restore-focus-after.ts
  136. var restoreFocusAfter = (cb) => {
  137. const el = document.activeElement;
  138. cb();
  139. el.focus();
  140. };
  141. // src/utils/until.ts
  142. var until = (getItem, check, timeout = 1e4, interval = 20) => new Promise((res, rej) => {
  143. let item = getItem();
  144. if (check(item))
  145. return res(item);
  146. const limit = timeout / interval;
  147. let i = 0;
  148. const id = setInterval(() => {
  149. item = getItem();
  150. if (check(item)) {
  151. clearInterval(id);
  152. res(item);
  153. } else if (++i > limit) {
  154. clearInterval(id);
  155. rej(new Error(`Timeout: item ${getItem.name} wasn't found`));
  156. }
  157. }, interval);
  158. });
  159. var untilAppear = (getItem, msToWait) => until(getItem, Boolean, msToWait);
  160. // src/utils/find-in-node-list.ts
  161. var findInNodeList = (list, predicate) => {
  162. for (const item of list) {
  163. if (predicate(item))
  164. return item;
  165. }
  166. return null;
  167. };
  168. // src/utils/get-el-creator.ts
  169. var getElCreator = (tag) => (props) => Object.assign(document.createElement(tag), props);
  170. // src/utils/delay.ts
  171. var delay = (ms) => new Promise((res) => setTimeout(res, ms));
  172. // src/element-getters.ts
  173. var plr = () => $("movie_player");
  174. var aboveTheFold = () => $("above-the-fold");
  175. var actionsBar = () => $("actions")?.querySelector("ytd-menu-renderer");
  176. var getPlrGetter = (plr2) => (selector) => () => plr2.querySelector(selector);
  177. var plrGetters = (plr2) => {
  178. const get = getPlrGetter(plr2);
  179. return {
  180. ad: get(".ytp-ad-player-overlay"),
  181. video: get(".html5-main-video"),
  182. subtitlesBtn: get(".ytp-subtitles-button"),
  183. muteBtn: get(".ytp-mute-button"),
  184. menu: {
  185. element: get(".ytp-settings-menu"),
  186. btn: get(".ytp-settings-button")
  187. }
  188. };
  189. };
  190. var plrMenuItemsGetter = (menu) => () => menu.querySelectorAll('.ytp-menuitem[role="menuitem"]');
  191. var menuSubItems = (item) => item.querySelectorAll(".ytp-menuitem-label");
  192. var channelUsernameElementGetter = (aboveTheFold2) => () => aboveTheFold2.querySelector(".ytd-channel-name > a");
  193. var artistChannelBadge = (aboveTheFold2) => aboveTheFold2.querySelector(".badge-style-type-verified-artist");
  194. var videoPlr = () => document.querySelector(".html5-video-player");
  195. var videoPlrCaptions = (plr2) => plr2.querySelectorAll(".captions-text > span");
  196. var popupContainer = () => document.querySelector("ytd-popup-container");
  197.  
  198. // src/player/menu.ts
  199. var set = (getEl) => {
  200. element ||= getEl.menu.element();
  201. btn ||= getEl.menu.btn();
  202. };
  203. var element = null;
  204. var btn = null;
  205. var clickBtn = () => {
  206. btn.click();
  207. };
  208. var isOpen = () => {
  209. return element.style.display !== "none";
  210. };
  211. var setOpen = (bool) => {
  212. if (bool !== isOpen())
  213. btn.click();
  214. };
  215. var openItem = (item) => {
  216. setOpen(true);
  217. item.click();
  218. return menuSubItems(element);
  219. };
  220. var settingItems = {
  221. speed: null,
  222. quality: null
  223. };
  224. var setSettingItems = (items) => {
  225. const findIcon = (d) => findInNodeList(items, (el) => !!el.querySelector(`path[d="${d}"]`));
  226. settingItems.speed = findIcon("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");
  227. settingItems.quality = findIcon("M15,17h6v1h-6V17z M11,17H3v1h8v2h1v-2v-1v-2h-1V17z M14,8h1V6V5V3h-1v2H3v1h11V8z M18,5v1h3V5H18z M6,14h1v-2v-1V9H6v2H3v1 h3V14z M10,12h11v-1H10V12z");
  228. };
  229. var findInItem = (name) => untilAppear(() => settingItems[name]).then((item) => (predicate) => {
  230. const oldSubItems = new Set(menuSubItems(element));
  231. return findInNodeList(openItem(item), (subItem) => !oldSubItems.has(subItem) && predicate(subItem));
  232. });
  233.  
  234. // src/player/plr.ts
  235. var setPlr = async (el) => {
  236. const getEl = plrGetters(el);
  237. await delay(1000);
  238. await until(getEl.ad, (ad) => !ad, 200000);
  239. video ||= getEl.video();
  240. subtitlesBtn ||= getEl.subtitlesBtn();
  241. muteBtn ||= getEl.muteBtn();
  242. set(getEl);
  243. restoreFocusAfter(clickBtn);
  244. await delay(50);
  245. restoreFocusAfter(clickBtn);
  246. setSettingItems(await until(plrMenuItemsGetter(element), (arr) => !!arr.length));
  247. if (!speedNormal)
  248. findInItem("speed").then((findInSpeed) => {
  249. restoreFocusAfter(() => {
  250. speedNormal = findInSpeed((btn2) => !+btn2.textContent).textContent;
  251. });
  252. });
  253. };
  254. var isSpeed = (value3) => video.playbackRate === value3;
  255. var speedNormal = "";
  256. var video = null;
  257. var subtitlesBtn = null;
  258. var muteBtn = null;
  259. // src/logger.ts
  260. var err = (...msgs) => {
  261. console.error("[YT-Defaulter]", ...msgs);
  262. };
  263. var outOfRange = (what) => {
  264. err(what, "value is out of range");
  265. };
  266.  
  267. // src/player/value-setters.ts
  268. var comparators = {
  269. quality: (target, current) => +target >= Number.parseInt(current) && (value.flags.enhancedBitrate || !current.toLowerCase().includes("premium")),
  270. speed: (target, current) => target === current
  271. };
  272. var setYT = (settingName) => async (value3) => {
  273. const isOpen2 = isOpen();
  274. const compare = comparators[settingName];
  275. const btn2 = (await findInItem(settingName))((btn3) => compare(value3, btn3.textContent));
  276. if (btn2) {
  277. btn2.click();
  278. }
  279. setOpen(isOpen2);
  280. };
  281. var valueSetters = {
  282. speed: (value3) => {
  283. setYT("speed")(isSpeed(+value3) ? speedNormal : value3);
  284. },
  285. customSpeed: (value3) => {
  286. try {
  287. video.playbackRate = isSpeed(+value3) ? 1 : +value3;
  288. } catch {
  289. outOfRange("Custom speed");
  290. }
  291. },
  292. subtitles: (value3) => {
  293. if (subtitlesBtn.ariaPressed !== value3.toString())
  294. subtitlesBtn.click();
  295. },
  296. volume: (value3) => {
  297. const num = +value3;
  298. const isMuted = muteBtn.dataset.titleNoTooltip !== "Mute";
  299. if (num === 0) {
  300. if (!isMuted)
  301. muteBtn.click();
  302. } else {
  303. if (isMuted)
  304. muteBtn.click();
  305. try {
  306. video.volume = num / 100;
  307. } catch {
  308. outOfRange("Volume");
  309. }
  310. }
  311. },
  312. quality: setYT("quality")
  313. };
  314. // src/player/apply-settings.ts
  315. var applySettings = (settings) => {
  316. if (!Number.isNaN(+settings.customSpeed)) {
  317. valueSetters.customSpeed(settings.customSpeed);
  318. }
  319. delete settings.customSpeed;
  320. restoreFocusAfter(() => {
  321. for (const setting in settings) {
  322. valueSetters[setting](settings[setting]);
  323. }
  324. setOpen(false);
  325. });
  326. };
  327. // src/compute-settings.ts
  328. var computeSettings = (doUseNormalSpeed) => {
  329. const channel2 = channel();
  330. const settings = {
  331. ...value.global,
  332. ...channel2
  333. };
  334. if (doUseNormalSpeed) {
  335. settings.speed = speedNormal;
  336. delete settings.customSpeed;
  337. } else if ("customSpeed" in channel2) {
  338. delete settings.speed;
  339. } else if ("speed" in channel2) {
  340. delete settings.customSpeed;
  341. } else if ("customSpeed" in settings) {
  342. delete settings.speed;
  343. }
  344. return settings;
  345. };
  346.  
  347. // src/listeners/keyup.ts
  348. var onKeyup = (e) => {
  349. if (e.code === "Enter") {
  350. onClick(e);
  351. } else if (e.ctrlKey && !e.shiftKey) {
  352. if (value.flags.copySubs && e.code === "KeyC") {
  353. const plr3 = videoPlr();
  354. if (plr3?.classList.contains("ytp-fullscreen")) {
  355. const text = Array.from(videoPlrCaptions(plr3), (line) => line.textContent).join(" ");
  356. navigator.clipboard.writeText(text);
  357. }
  358. } else if (e.code === "Space") {
  359. e.stopPropagation();
  360. e.preventDefault();
  361. const settings = computeSettings(false);
  362. if (settings.speed) {
  363. restoreFocusAfter(() => {
  364. valueSetters.speed(settings.speed);
  365. });
  366. } else if (settings.customSpeed) {
  367. valueSetters.customSpeed(settings.customSpeed);
  368. }
  369. }
  370. }
  371. };
  372. // src/text.ts
  373. var translations = {
  374. "be-BY": {
  375. OPEN_SETTINGS: "Адкрыць дадатковыя налады",
  376. SUBTITLES: "Субтытры",
  377. SPEED: "Хуткасьць",
  378. CUSTOM_SPEED: "Свая Хуткасьць",
  379. CUSTOM_SPEED_HINT: 'Калі вызначана, будзе выкарыстоўвацца замест "Хуткасьць"',
  380. QUALITY: "Якасьць",
  381. VOLUME: "Гучнасьць, %",
  382. GLOBAL: "Глябальна",
  383. LOCAL: "Гэты Канал",
  384. SHORTS: "Адкрываць shorts як звычайныя",
  385. NEW_TAB: "Адкрываць відэа ў новай картцы",
  386. COPY_SUBS: "Капіяваць субтытры ў поўнаэкранным, Ctrl+C",
  387. STANDARD_MUSIC_SPEED: "Звычайная хуткасьць на каналах музыкаў",
  388. ENHANCED_BITRATE: "Палепшаны бітрэйт (для карыстальнікаў Premium)",
  389. HIDE_SHORTS: "Схаваць shorts на галоўнай",
  390. SAVE: "Захаваць",
  391. EXPORT: "Экспарт",
  392. IMPORT: "Імпарт"
  393. }
  394. };
  395. var text = {
  396. OPEN_SETTINGS: "Open additional settings",
  397. SUBTITLES: "Subtitles",
  398. SPEED: "Speed",
  399. CUSTOM_SPEED: "Custom Speed",
  400. CUSTOM_SPEED_HINT: 'If defined, will be used instead of "Speed"',
  401. QUALITY: "Quality",
  402. VOLUME: "Volume, %",
  403. GLOBAL: "Global",
  404. LOCAL: "This Channel",
  405. SHORTS: "Open shorts as regular videos",
  406. NEW_TAB: "Open videos in new tab",
  407. COPY_SUBS: "Copy subtitles by Ctrl+C in fullscreen mode",
  408. STANDARD_MUSIC_SPEED: "Normal speed on artist channels",
  409. ENHANCED_BITRATE: "Quality: Enhanced bitrate (for Premium users)",
  410. HIDE_SHORTS: "Hide shorts on the Home page",
  411. SAVE: "Save",
  412. DEFAULT: "-",
  413. EXPORT: "Export",
  414. IMPORT: "Import"
  415. };
  416.  
  417. // src/menu/controls.ts
  418. var updateValuesIn = (controls, cfgPart) => {
  419. controls.speed.value = cfgPart.speed || text.DEFAULT;
  420. controls.customSpeed.value = cfgPart.customSpeed || "";
  421. controls.quality.value = cfgPart.quality || text.DEFAULT;
  422. controls.volume.value = cfgPart.volume || "";
  423. controls.subtitles.checked = cfgPart.subtitles || false;
  424. };
  425. var channelControls = () => ({
  426. speed: null,
  427. customSpeed: null,
  428. quality: null,
  429. volume: null,
  430. subtitles: null
  431. });
  432. var sections = {
  433. global: channelControls(),
  434. thisChannel: channelControls()
  435. };
  436. var flags = {
  437. shortsToRegular: {
  438. elem: null,
  439. id: "shorts",
  440. text: text.SHORTS
  441. },
  442. newTab: {
  443. elem: null,
  444. id: "new-tab",
  445. text: text.NEW_TAB
  446. },
  447. copySubs: {
  448. elem: null,
  449. id: "copy-subs",
  450. text: text.COPY_SUBS
  451. },
  452. standardMusicSpeed: {
  453. elem: null,
  454. id: "standard-music-speed",
  455. text: text.STANDARD_MUSIC_SPEED
  456. },
  457. enhancedBitrate: {
  458. elem: null,
  459. id: "enhanced-bitrate",
  460. text: text.ENHANCED_BITRATE
  461. },
  462. hideShorts: {
  463. elem: null,
  464. id: "hide-shorts",
  465. text: text.HIDE_SHORTS
  466. }
  467. };
  468. var updateThisChannel = () => {
  469. updateValuesIn(sections.thisChannel, channel());
  470. };
  471. var updateValues = (cfg) => {
  472. updateValuesIn(sections.global, cfg.global);
  473. updateThisChannel();
  474. for (const key in cfg.flags) {
  475. const flag = flags[key];
  476. if (flag) {
  477. flag.elem.checked = cfg.flags[key];
  478. } else {
  479. err("Unknown flag:", key);
  480. }
  481. }
  482. };
  483. // src/utils/with.ts
  484. var withHint = (hint, getItem) => [getItem(hint).item, hint.element];
  485. var withOnClick = (elem, listener) => {
  486. elem.addEventListener("click", listener);
  487. return elem;
  488. };
  489. var withListeners = (elem, listeners) => {
  490. for (const key in listeners) {
  491. elem.addEventListener(key, listeners[key]);
  492. }
  493. return elem;
  494. };
  495. var controlWith = (withFn) => (obj, ...args) => {
  496. withFn(obj.elem, ...args);
  497. return obj;
  498. };
  499. var withControlListeners = controlWith(withListeners);
  500.  
  501. // src/utils/element-creators.ts
  502. var div = getElCreator("div");
  503. var input = getElCreator("input");
  504. var checkbox = (props) => input({ type: "checkbox", ...props });
  505. var option = getElCreator("option");
  506. var _label = getElCreator("label");
  507. var labelEl = (forId, props) => {
  508. const elem = _label(props);
  509. elem.setAttribute("for", forId);
  510. return elem;
  511. };
  512. var selectEl = getElCreator("select");
  513. var btnClass = "yt-spec-button-shape-next";
  514. var btnClassFocused = btnClass + "--focused";
  515. var _button = getElCreator("button");
  516. var button = (textContent, props) => withListeners(_button({
  517. textContent,
  518. className: `${btnClass} ${btnClass}--tonal ${btnClass}--mono ${btnClass}--size-m`,
  519. ...props
  520. }), {
  521. focus() {
  522. this.classList.add(btnClassFocused);
  523. },
  524. blur() {
  525. this.classList.remove(btnClassFocused);
  526. }
  527. });
  528.  
  529. // src/hint.ts
  530. class Hint {
  531. constructor(prefix) {
  532. this.element = div();
  533. this.element.className ||= "YTDef-setting-hint";
  534. this.prefix = prefix;
  535. this.hide();
  536. }
  537. hide() {
  538. this.element.style.display = "none";
  539. }
  540. show(msg) {
  541. this.element.style.display = "block";
  542. this.element.textContent = this.prefix + msg;
  543. }
  544. prefix;
  545. element;
  546. }
  547.  
  548. // src/menu/get-controls-creators.ts
  549. var getControlCreators = (getCreator) => ({
  550. numericInput: getCreator((props) => input({ type: "number", ...props }), (elem) => ({
  551. get: () => elem.value,
  552. set: (value3) => {
  553. elem.value = value3;
  554. },
  555. default: ""
  556. })),
  557. checkbox: getCreator(checkbox, (elem) => ({
  558. get: () => elem.checked.toString(),
  559. set: (value3) => {
  560. elem.checked = value3 === "true";
  561. },
  562. default: text.DEFAULT
  563. })),
  564. select: getCreator(({
  565. values,
  566. getText
  567. }) => {
  568. const elem = selectEl({ value: text.DEFAULT });
  569. elem.append(option({
  570. value: text.DEFAULT,
  571. textContent: text.DEFAULT
  572. }), ...values.map((value3) => option({
  573. value: value3,
  574. textContent: getText(value3)
  575. })));
  576. return elem;
  577. }, (elem) => ({
  578. get: () => elem.value,
  579. set: (value3) => {
  580. elem.value = value3;
  581. },
  582. default: "false"
  583. }))
  584. });
  585.  
  586. // src/menu/validate-volume.ts
  587. var validateVolume = (value3) => {
  588. const num = +value3;
  589. return num < 0 || num > 100 ? "out of range" : Number.isNaN(num) ? "not a number" : null;
  590. };
  591.  
  592. // src/menu/close.ts
  593. var close = () => {
  594. element2.style.visibility = "hidden";
  595. stopListening();
  596. };
  597. var listenForClose = () => {
  598. document.addEventListener("click", onClick2);
  599. document.addEventListener("keyup", onKeyUp);
  600. };
  601. var stopListening = () => {
  602. document.removeEventListener("click", onClick2);
  603. document.removeEventListener("keyup", onKeyUp);
  604. };
  605. var onClick2 = (e) => {
  606. const el = e.target;
  607. if (!isDescendantOrTheSame(el, [element2, btn2]))
  608. close();
  609. };
  610. var onKeyUp = (e) => {
  611. if (e.code === "Escape") {
  612. close();
  613. btn2.focus();
  614. }
  615. };
  616.  
  617. // src/menu/value.ts
  618. var set2 = (el, btnEl) => {
  619. element2 = el;
  620. btn2 = btnEl;
  621. };
  622. var element2 = null;
  623. var btn2 = null;
  624. var isOpen2 = false;
  625. var menuWidth = 0;
  626. var adjustWidth = () => {
  627. menuWidth = element2.getBoundingClientRect().width;
  628. };
  629. var firstFocusable = ref(null);
  630. var toggle = debounce(() => {
  631. isOpen2 = !isOpen2;
  632. if (isOpen2) {
  633. fixPosition();
  634. element2.style.visibility = "visible";
  635. listenForClose();
  636. firstFocusable.val.focus();
  637. } else {
  638. close();
  639. }
  640. }, 100);
  641. var fixPosition = () => {
  642. const { y, height, width, left } = btn2.getBoundingClientRect();
  643. element2.style.top = y + height + 8 + "px";
  644. element2.style.left = left + width - menuWidth + "px";
  645. };
  646.  
  647. // src/menu/section.ts
  648. var section = (sectionId, title, sectionCfg) => {
  649. const control = getControlCreators((createElement, initVal) => (name, label, props) => {
  650. const item = div();
  651. const id = "YTDef-" + name + "-" + sectionId;
  652. const elem = Object.assign(createElement(props), props, {
  653. id,
  654. name
  655. });
  656. elem.addEventListener("change", () => {
  657. const value3 = val.get();
  658. if (value3 === val.default) {
  659. delete sectionCfg[name];
  660. } else {
  661. sectionCfg[name] = value3;
  662. }
  663. });
  664. const val = initVal(elem);
  665. const cfgValue = sectionCfg[name];
  666. if (cfgValue) {
  667. setTimeout(() => {
  668. val.set(cfgValue.toString());
  669. });
  670. }
  671. item.append(labelEl(id, { textContent: label }), elem);
  672. sections[sectionId][name] = elem;
  673. return { item, elem };
  674. });
  675. const speedSelect = control.select("speed", text.SPEED, {
  676. values: ["2", "1.75", "1.5", "1.25", speedNormal, "0.75", "0.5", "0.25"],
  677. getText: (val) => val
  678. });
  679. if (sectionId === "global")
  680. firstFocusable.val = speedSelect.elem;
  681. const sectionElement = div({ role: "group" });
  682. sectionElement.setAttribute("aria-labelledby", sectionId);
  683. sectionElement.append(getElCreator("span")({ textContent: title, id: sectionId }), speedSelect.item, ...withHint(new Hint(""), (hint) => withControlListeners(control.numericInput("customSpeed", text.CUSTOM_SPEED), {
  684. blur: () => {
  685. hint.hide();
  686. },
  687. focus: () => {
  688. hint.show(text.CUSTOM_SPEED_HINT);
  689. }
  690. })), control.select("quality", text.QUALITY, {
  691. values: [
  692. "144",
  693. "240",
  694. "360",
  695. "480",
  696. "720",
  697. "1080",
  698. "1440",
  699. "2160",
  700. "4320"
  701. ],
  702. getText: (val) => val + "p"
  703. }).item, ...withHint(new Hint("Warning: "), (hint) => withControlListeners(control.numericInput("volume", text.VOLUME, {
  704. min: "0",
  705. max: "100"
  706. }), {
  707. blur() {
  708. const warning = validateVolume(this.value);
  709. if (warning) {
  710. hint.show(warning);
  711. } else {
  712. hint.hide();
  713. }
  714. }
  715. })), control.checkbox("subtitles", text.SUBTITLES, checkbox()).item);
  716. return sectionElement;
  717. };
  718.  
  719. // src/menu/settings-icon.ts
  720. var settingsIcon = () => {
  721. const element3 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  722. for (const [prop, value3] of [
  723. ["viewBox", "0 0 24 24"],
  724. ["width", "24"],
  725. ["height", "24"],
  726. ["fill", "var(--yt-spec-text-primary)"]
  727. ]) {
  728. element3.setAttribute(prop, value3);
  729. }
  730. element3.append($("settings"));
  731. return element3;
  732. };
  733.  
  734. // src/menu/init.ts
  735. var init = () => {
  736. const sections2 = div({ className: "YTDef-" + "sections" });
  737. sections2.append(section("global", text.GLOBAL, value.global), section("thisChannel", text.LOCAL, channel()));
  738. const controlStatus = div();
  739. const updateControlStatus = (content) => {
  740. controlStatus.textContent = `[${new Date().toLocaleTimeString()}] ${content}`;
  741. };
  742. const controlDiv = div({ className: "control-cont" });
  743. controlDiv.append(withOnClick(button(text.SAVE), () => {
  744. prune();
  745. saveLS(value);
  746. updateControlStatus(text.SAVE);
  747. }), withOnClick(button(text.EXPORT), () => {
  748. navigator.clipboard.writeText(localStorage.YTDefaulter).then(() => {
  749. updateControlStatus(text.EXPORT);
  750. });
  751. }), withOnClick(button(text.IMPORT), () => {
  752. navigator.clipboard.readText().then((raw) => {
  753. save(raw);
  754. updateValues(value);
  755. return text.IMPORT;
  756. }).catch((e) => text.IMPORT + ": " + e.message).then(updateControlStatus);
  757. }));
  758. set2(div({
  759. id: "YTDef-menu"
  760. }), withOnClick(button("", {
  761. id: "YTDef-btn",
  762. ariaLabel: text.OPEN_SETTINGS,
  763. tabIndex: 0
  764. }), toggle));
  765. btn2.setAttribute("aria-controls", "YTDef-menu");
  766. btn2.classList.add(btnClass + "--icon-button");
  767. btn2.append(settingsIcon());
  768. element2.append(sections2, ...Object.entries(flags).map(([flagName, flag]) => {
  769. const cont = div({ className: "check-cont" });
  770. const id = "YTDef-" + flag.id;
  771. const elem = withOnClick(checkbox({
  772. id,
  773. checked: value.flags[flagName]
  774. }), function() {
  775. value.flags[flagName] = this.checked;
  776. });
  777. flag.elem = elem;
  778. cont.append(labelEl(id, { textContent: flag.text }), elem);
  779. return cont;
  780. }), controlDiv, controlStatus);
  781. element2.addEventListener("keyup", (e) => {
  782. const el = e.target;
  783. if (e.code === "Enter" && el.type === "checkbox")
  784. el.checked = !el.checked;
  785. });
  786. untilAppear(actionsBar).then((actionsBar2) => {
  787. actionsBar2.insertBefore(btn2, actionsBar2.lastChild);
  788. popupContainer().append(element2);
  789. adjustWidth();
  790. sections2.style.maxWidth = sections2.offsetWidth + "px";
  791. });
  792. const listener = () => {
  793. if (isOpen2)
  794. fixPosition();
  795. };
  796. window.addEventListener("scroll", listener);
  797. window.addEventListener("resize", listener);
  798. };
  799. // src/listeners/video-page.ts
  800. var onVideoPage = async () => {
  801. const aboveTheFold2 = await untilAppear(aboveTheFold);
  802. username.val = (await untilAppear(channelUsernameElementGetter(aboveTheFold2))).href || "";
  803. untilAppear(plr).then(setPlr).then(() => {
  804. const doNotChangeSpeed = value.flags.standardMusicSpeed && !!artistChannelBadge(aboveTheFold2);
  805. applySettings(computeSettings(doNotChangeSpeed));
  806. if (!element2) {
  807. init();
  808. }
  809. });
  810. if (element2) {
  811. updateThisChannel();
  812. }
  813. };
  814. // src/style.ts
  815. var m = "#" + "YTDef-menu";
  816. var d = " div";
  817. var i = " input";
  818. var s = " select";
  819. var bg = "var(--yt-spec-menu-background)";
  820. var underline = "border-bottom: 2px solid var(--yt-spec-text-primary);";
  821. var style = getElCreator("style")({
  822. textContent: `
  823. #${"YTDef-btn"} {position: relative; margin-left: 8px}
  824. ${m} {
  825. display: flex;
  826. visibility: hidden;
  827. color: var(--yt-spec-text-primary);
  828. font-size: 14px;
  829. flex-direction: column;
  830. position: fixed;
  831. background: ${bg};
  832. border-radius: 2rem;
  833. padding: 1rem;
  834. text-align: center;
  835. box-shadow: 0px 4px 32px 0px var(--yt-spec-static-overlay-background-light);
  836. z-index: 2202
  837. }
  838. .control-cont > button {margin: .2rem}
  839. ${m + d} {display: flex; margin-bottom: 1rem}
  840. ${m + d + d} {
  841. flex-direction: column;
  842. margin: 0 2rem
  843. }
  844. ${m + d + d + d} {
  845. flex-direction: row;
  846. margin: 1rem 0
  847. }
  848. ${m + s}, ${m + i} {
  849. text-align: center;
  850. background: ${bg};
  851. border: none;
  852. ${underline}
  853. color: inherit;
  854. width: 5rem;
  855. padding: 0;
  856. margin-left: auto
  857. }
  858. ${m} .${"YTDef-setting-hint"} {margin: 0; text-align: end}
  859. ${m + i} {outline: none}
  860. ${m + d + d + d}:focus-within > label, ${m} .check-cont:focus-within > label {${underline}}
  861. ${m} .check-cont {padding: 0 1rem}
  862. ${m + s} {appearance: none; outline: none}
  863. ${m} label {margin-right: 1.5rem; white-space: nowrap}
  864. ${m + i}::-webkit-outer-spin-button,
  865. ${m + i}::-webkit-inner-spin-button {-webkit-appearance: none; margin: 0}
  866. ${m + i}[type=number] {-moz-appearance: textfield}
  867. ${m + s}::-ms-expand {display: none}`
  868. });
  869.  
  870. // src/index.ts
  871. Object.assign(text, translations[document.documentElement.lang]);
  872. if (update(value)) {
  873. saveLS(value);
  874. }
  875. window.addEventListener("yt-navigate-finish", ({ detail: { pageType } }) => {
  876. if (pageType === "watch" || pageType === "live") {
  877. setTimeout(onVideoPage, 1000);
  878. }
  879. });
  880. setInterval(() => {
  881. if (value.flags.hideShorts)
  882. findInNodeList(document.querySelectorAll("#title"), (el) => el.textContent === "Shorts")?.closest("ytd-rich-section-renderer")?.remove();
  883. }, 1000);
  884. document.addEventListener("click", onClick, { capture: true });
  885. document.addEventListener("keyup", onKeyup, { capture: true });
  886. document.head.append(style);
  887. })()