input.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { 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. view: unsafeWindow,
  34. };
  35. let invalidKey = false;
  36. let keyProps = {};
  37. switch(evt.code) {
  38. case "ArrowLeft":
  39. keyProps = {
  40. code: "KeyH",
  41. key: "h",
  42. keyCode: 72,
  43. which: 72,
  44. };
  45. break;
  46. case "ArrowRight":
  47. keyProps = {
  48. code: "KeyL",
  49. key: "l",
  50. keyCode: 76,
  51. which: 76,
  52. };
  53. break;
  54. default:
  55. invalidKey = true;
  56. break;
  57. }
  58. if(!invalidKey) {
  59. // TODO: check if the code prop is correct
  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. setTimeout(() => { location.href = url; }, 0);
  99. }
  100. catch(err) {
  101. console.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. /** Adds a spy function into `window.__proto__.addEventListener` to selectively discard events before they can be captured by the original site's listeners */
  117. export function initBeforeUnloadHook() {
  118. Error.stackTraceLimit = Infinity;
  119. (function(original) {
  120. // @ts-ignore
  121. window.__proto__.addEventListener = function(...args) {
  122. const [type, listener, ...rest] = args;
  123. if(type === "beforeunload") {
  124. return original.apply(this, [
  125. type,
  126. // @ts-ignore
  127. (...a) => {
  128. if(beforeUnloadEnabled)
  129. listener(...a);
  130. },
  131. ...rest,
  132. ]);
  133. }
  134. else
  135. return original.apply(this, args);
  136. };
  137. // @ts-ignore
  138. })(window.__proto__.addEventListener);
  139. getFeatures().then(feats => {
  140. if(feats.disableBeforeUnloadPopup)
  141. disableBeforeUnload();
  142. });
  143. // (function(original) {
  144. // window.__proto__.removeEventListener = function(type, listener, useCapture) {
  145. // if(evtNames.includes(type)){
  146. // console.log("------> removeEventListener " + type, listener, useCapture);
  147. // }
  148. // return original.apply(this, arguments);
  149. // };
  150. // })(window.__proto__.removeEventListener);
  151. }