input.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { getUnsafeWindow } from "@sv443-network/userutils";
  2. import { error, getVideoTime, info, log, warn } from "../utils";
  3. import type { Domain } from "../types";
  4. import { getFeatures } from "../config";
  5. //#MARKER arrow key skip
  6. export function initArrowKeySkip() {
  7. document.addEventListener("keydown", onKeyDown);
  8. log("Added key press listener");
  9. }
  10. /** Called when the user presses any key, anywhere */
  11. function onKeyDown(evt: KeyboardEvent) {
  12. if(["ArrowLeft", "ArrowRight"].includes(evt.code)) {
  13. // discard the event when a (text) input is currently active, like when editing a playlist
  14. if(["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement?.tagName ?? "_"))
  15. return info(`Captured valid key but the current active element is <${document.activeElement!.tagName.toLowerCase()}>, so the keypress is ignored`);
  16. log(`Captured key '${evt.code}' in proxy listener`);
  17. // ripped this stuff from the console, most of these are probably unnecessary but this was finnicky af and I am sick and tired of trial and error
  18. const defaultProps = {
  19. altKey: false,
  20. ctrlKey: false,
  21. metaKey: false,
  22. shiftKey: false,
  23. target: document.body,
  24. currentTarget: document.body,
  25. originalTarget: document.body,
  26. explicitOriginalTarget: document.body,
  27. srcElement: document.body,
  28. type: "keydown",
  29. bubbles: true,
  30. cancelBubble: false,
  31. cancelable: true,
  32. isTrusted: true,
  33. repeat: false,
  34. // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
  35. view: getUnsafeWindow(),
  36. };
  37. let invalidKey = false;
  38. let keyProps = {};
  39. switch(evt.code) {
  40. case "ArrowLeft":
  41. keyProps = {
  42. code: "KeyH",
  43. key: "h",
  44. keyCode: 72,
  45. which: 72,
  46. };
  47. break;
  48. case "ArrowRight":
  49. keyProps = {
  50. code: "KeyL",
  51. key: "l",
  52. keyCode: 76,
  53. which: 76,
  54. };
  55. break;
  56. default:
  57. invalidKey = true;
  58. break;
  59. }
  60. if(!invalidKey) {
  61. const proxyProps = { code: "", ...defaultProps, ...keyProps };
  62. document.body.dispatchEvent(new KeyboardEvent("keydown", proxyProps));
  63. log(`Dispatched proxy keydown event: [${evt.code}] -> [${proxyProps.code}]`);
  64. }
  65. else
  66. warn(`Captured key '${evt.code}' has no defined behavior`);
  67. }
  68. }
  69. //#MARKER site switch
  70. /** switch sites only if current video time is greater than this value */
  71. const videoTimeThreshold = 3;
  72. /** Initializes the site switch feature */
  73. export function initSiteSwitch(domain: Domain) {
  74. document.addEventListener("keydown", (e) => {
  75. if(e.key === "F9")
  76. switchSite(domain === "yt" ? "ytm" : "yt");
  77. });
  78. log("Initialized site switch listener");
  79. }
  80. /** Switches to the other site (between YT and YTM) */
  81. async function switchSite(newDomain: Domain) {
  82. try {
  83. if(newDomain === "ytm" && !location.href.includes("/watch"))
  84. return warn("Not on a video page, so the site switch is ignored");
  85. let subdomain;
  86. if(newDomain === "ytm")
  87. subdomain = "music";
  88. else if(newDomain === "yt")
  89. subdomain = "www";
  90. if(!subdomain)
  91. throw new Error(`Unrecognized domain '${newDomain}'`);
  92. disableBeforeUnload();
  93. const { pathname, search, hash } = new URL(location.href);
  94. const vt = await getVideoTime();
  95. log(`Found video time of ${vt} seconds`);
  96. const cleanSearch = search.split("&")
  97. .filter((param) => !param.match(/^\??t=/))
  98. .join("&");
  99. const newSearch = typeof vt === "number" && vt > videoTimeThreshold ?
  100. cleanSearch.includes("?")
  101. ? `${cleanSearch.startsWith("?")
  102. ? cleanSearch
  103. : "?" + cleanSearch
  104. }&t=${vt}`
  105. : `?t=${vt}`
  106. : cleanSearch;
  107. const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
  108. info(`Switching to domain '${newDomain}' at ${newUrl}`);
  109. location.assign(newUrl);
  110. }
  111. catch(err) {
  112. error("Error while switching site:", err);
  113. }
  114. }
  115. //#MARKER beforeunload popup
  116. let beforeUnloadEnabled = true;
  117. /** Disables the popup before leaving the site */
  118. export function disableBeforeUnload() {
  119. beforeUnloadEnabled = false;
  120. info("Disabled popup before leaving the site");
  121. }
  122. /** (Re-)enables the popup before leaving the site */
  123. export function enableBeforeUnload() {
  124. beforeUnloadEnabled = true;
  125. info("Enabled popup before leaving the site");
  126. }
  127. /**
  128. * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
  129. * event listeners before they can be called by the site.
  130. */
  131. export function initBeforeUnloadHook() {
  132. Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
  133. (function(original: typeof window.addEventListener) {
  134. // @ts-ignore
  135. window.__proto__.addEventListener = function(...args: Parameters<typeof window.addEventListener>) {
  136. if(!beforeUnloadEnabled && args[0] === "beforeunload")
  137. return info("Prevented beforeunload event listener from being called");
  138. else
  139. return original.apply(this, args);
  140. };
  141. // @ts-ignore
  142. })(window.__proto__.addEventListener);
  143. getFeatures().then(feats => {
  144. if(feats.disableBeforeUnloadPopup)
  145. disableBeforeUnload();
  146. });
  147. }