input.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { clamp } from "@sv443-network/userutils";
  2. import { error, getVideoTime, info, log, warn, getVideoSelector } from "../utils";
  3. import type { Domain } from "../types";
  4. import { isCfgMenuOpen } from "../menu/menu_old";
  5. import { disableBeforeUnload } from "./behavior";
  6. import { siteEvents } from "../siteEvents";
  7. import { featInfo } from "./index";
  8. import { getFeatures } from "../config";
  9. export const inputIgnoreTagNames = ["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A"];
  10. //#region arrow key skip
  11. export async function initArrowKeySkip() {
  12. document.addEventListener("keydown", (evt) => {
  13. if(!getFeatures().arrowKeySupport)
  14. return;
  15. if(!["ArrowLeft", "ArrowRight"].includes(evt.code))
  16. return;
  17. const allowedClasses = ["bytm-generic-btn", "yt-spec-button-shape-next"];
  18. // discard the event when a (text) input is currently active, like when editing a playlist
  19. if(
  20. (inputIgnoreTagNames.includes(document.activeElement?.tagName ?? "") || ["volume-slider"].includes(document.activeElement?.id ?? ""))
  21. && !allowedClasses.some((cls) => document.activeElement?.classList.contains(cls))
  22. )
  23. return info(`Captured valid key to skip forward or backward but the current active element is <${document.activeElement?.tagName.toLowerCase()}>, so the keypress is ignored`);
  24. evt.preventDefault();
  25. evt.stopImmediatePropagation();
  26. let skipBy = getFeatures().arrowKeySkipBy ?? featInfo.arrowKeySkipBy.default;
  27. if(evt.code === "ArrowLeft")
  28. skipBy *= -1;
  29. log(`Captured arrow key '${evt.code}' - skipping by ${skipBy} seconds`);
  30. const vidElem = document.querySelector<HTMLVideoElement>(getVideoSelector());
  31. if(vidElem)
  32. vidElem.currentTime = clamp(vidElem.currentTime + skipBy, 0, vidElem.duration);
  33. });
  34. log("Added arrow key press listener");
  35. }
  36. //#region site switch
  37. /** switch sites only if current video time is greater than this value */
  38. const videoTimeThreshold = 3;
  39. let siteSwitchEnabled = true;
  40. /** Initializes the site switch feature */
  41. export async function initSiteSwitch(domain: Domain) {
  42. document.addEventListener("keydown", (e) => {
  43. if(!getFeatures().switchBetweenSites)
  44. return;
  45. const hk = getFeatures().switchSitesHotkey;
  46. if(siteSwitchEnabled && e.code === hk.code && e.shiftKey === hk.shift && e.ctrlKey === hk.ctrl && e.altKey === hk.alt)
  47. switchSite(domain === "yt" ? "ytm" : "yt");
  48. });
  49. siteEvents.on("hotkeyInputActive", (state) => {
  50. if(!getFeatures().switchBetweenSites)
  51. return;
  52. siteSwitchEnabled = !state;
  53. });
  54. log("Initialized site switch listener");
  55. }
  56. /** Switches to the other site (between YT and YTM) */
  57. async function switchSite(newDomain: Domain) {
  58. try {
  59. if(!(["/watch", "/playlist"].some(v => location.pathname.startsWith(v))))
  60. return warn("Not on a supported page, so the site switch is ignored");
  61. let subdomain;
  62. if(newDomain === "ytm")
  63. subdomain = "music";
  64. else if(newDomain === "yt")
  65. subdomain = "www";
  66. if(!subdomain)
  67. throw new Error(`Unrecognized domain '${newDomain}'`);
  68. disableBeforeUnload();
  69. const { pathname, search, hash } = new URL(location.href);
  70. const vt = await getVideoTime(0);
  71. log(`Found video time of ${vt} seconds`);
  72. const cleanSearch = search.split("&")
  73. .filter((param) => !param.match(/^\??t=/))
  74. .join("&");
  75. const newSearch = typeof vt === "number" && vt > videoTimeThreshold ?
  76. cleanSearch.includes("?")
  77. ? `${cleanSearch.startsWith("?")
  78. ? cleanSearch
  79. : "?" + cleanSearch
  80. }&t=${vt}`
  81. : `?t=${vt}`
  82. : cleanSearch;
  83. const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
  84. info(`Switching to domain '${newDomain}' at ${newUrl}`);
  85. location.assign(newUrl);
  86. }
  87. catch(err) {
  88. error("Error while switching site:", err);
  89. }
  90. }
  91. //#region num keys skip
  92. const numKeysIgnoreTagNames = [...inputIgnoreTagNames, "TP-YT-PAPER-TAB"];
  93. const numKeysIgnoreIds = ["progress-bar", "song-media-window"];
  94. /** Adds the ability to skip to a certain time in the video by pressing a number key (0-9) */
  95. export async function initNumKeysSkip() {
  96. document.addEventListener("keydown", (e) => {
  97. if(!getFeatures().numKeysSkipToTime)
  98. return;
  99. if(!e.key.trim().match(/^[0-9]$/))
  100. return;
  101. if(isCfgMenuOpen)
  102. return;
  103. // discard the event when an unexpected element is currently active or in focus, like when editing a playlist or when the search bar is focused
  104. if(
  105. document.activeElement !== document.body // short-circuit if nothing is active
  106. || numKeysIgnoreIds.includes(document.activeElement?.id ?? "") // video element or player bar active
  107. || numKeysIgnoreTagNames.includes(document.activeElement?.tagName ?? "") // other element active
  108. )
  109. return info("Captured valid key to skip video to, but ignored it since an unexpected element is active:", document.activeElement);
  110. const vidElem = document.querySelector<HTMLVideoElement>(getVideoSelector());
  111. if(!vidElem)
  112. return warn("Could not find video element, so the keypress is ignored");
  113. const newVidTime = vidElem.duration / (10 / Number(e.key));
  114. if(!isNaN(newVidTime)) {
  115. log(`Captured number key [${e.key}], skipping to ${Math.floor(newVidTime / 60)}m ${(newVidTime % 60).toFixed(1)}s`);
  116. vidElem.currentTime = newVidTime;
  117. }
  118. });
  119. log("Added number key press listener");
  120. }