input.ts 5.0 KB

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