input.ts 4.7 KB

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