Răsfoiți Sursa

feat: add window onbeforeunload intercept feature

Sv443 1 an în urmă
părinte
comite
bf305ba160
4 a modificat fișierele cu 86 adăugiri și 10 ștergeri
  1. 10 8
      src/BetterYTM.user.ts
  2. 6 0
      src/features/index.ts
  3. 59 2
      src/features/input.ts
  4. 11 0
      src/types.d.ts

+ 10 - 8
src/BetterYTM.user.ts

@@ -11,20 +11,17 @@ import {
   // input
   initArrowKeySkip, initSiteSwitch, addAnchorImprovements,
   // menu
-  initMenu, addMenu,
+  initMenu, addMenu, initBeforeUnloadHook,
 } from "./features/index";
 
+// TODO: add some style
+console.log(`${info.name} v${info.version} (${info.lastCommit}) - ${info.namespace}`);
+console.log(`Powered by lots of ambition and my song metadata API: ${geniUrlBase}`);
+
 async function init() {
   await preInitLayout();
 
   try {
-    // TODO: add some style
-    console.log(`${info.name} v${info.version} (${info.lastCommit}) - ${info.namespace}`);
-    console.log(`Powered by lots of ambition and my song metadata API: ${geniUrlBase}`);
-
-    // post-build these double quotes are replaced by backticks
-    addGlobalStyle("{{GLOBAL_STYLE}}", "global");
-
     document.addEventListener("DOMContentLoaded", onDomLoad);
   }
   catch(err) {
@@ -41,6 +38,9 @@ async function init() {
 
 /** Called when the DOM has finished loading and can be queried and altered by the userscript */
 async function onDomLoad() {
+  // post-build these double quotes are replaced by backticks
+  addGlobalStyle("{{GLOBAL_STYLE}}", "global");
+
   const features = await getFeatures();
   const domain = getDomain();
 
@@ -91,4 +91,6 @@ async function onDomLoad() {
   }
 }
 
+// needs to be called ASAP, before anything async happens
+initBeforeUnloadHook();
 init();

+ 6 - 0
src/features/index.ts

@@ -34,6 +34,12 @@ export const featInfo = Object.freeze({
       meta: false,
     },
   },
+  disableBeforeUnloadPopup: {
+    desc: "Whether to completely disable the popup that sometimes appears before leaving the site",
+    type: "toggle",
+    category: "input",
+    default: false,
+  },
   anchorImprovements: {
     desc: "TODO: Make it so middle clicking a song to open it in a new tab is easier",
     type: "toggle",

+ 59 - 2
src/features/input.ts

@@ -1,6 +1,7 @@
 import { getVideoTime } from "../utils";
 import { dbg } from "../constants";
 import type { Domain } from "../types";
+import { getFeatures } from "../config";
 
 //#MARKER arrow key skip
 
@@ -83,9 +84,10 @@ export function initSiteSwitch(domain: Domain) {
   // TODO:
   // extra features:
   // - keep video time
+  // - configurable hotkey
 
   document.addEventListener("keydown", (e) => {
-    if(e.key == "F9")
+    if(e.key === "F9")
       switchSite(domain === "yt" ? "ytm" : "yt");
   });
   dbg && console.log("BetterYTM: Initialized site switch listener");
@@ -116,9 +118,64 @@ function switchSite(newDomain: Domain) {
 
     console.info(`BetterYTM - switching to domain '${newDomain}' at ${url}`);
 
-    location.href = url;
+    disableBeforeUnload();
+    setImmediate(() => location.href = url);
   }
   catch(err) {
     console.error("BetterYTM: Error while switching site:", err);
   }
 }
+
+//#MARKER beforeunload popup
+
+let beforeUnloadEnabled = true;
+
+/** Disables the popup before leaving the site */
+export function disableBeforeUnload() {
+  beforeUnloadEnabled = false;
+  dbg && console.info("BetterYTM: Disabled popup before leaving the site");
+}
+
+/** (Re-)enables the popup before leaving the site */
+export function enableBeforeUnload() {
+  beforeUnloadEnabled = true;
+  dbg && console.info("BetterYTM: Enabled popup before leaving the site");
+}
+
+/** Adds a spy function into `window.__proto__.addEventListener` to selectively discard events before they can be captured by the original site's listeners */
+export function initBeforeUnloadHook() {
+  Error.stackTraceLimit = Infinity;
+
+  (function(original) {
+    window.__proto__.addEventListener = function(...args) {
+      const [type, listener, ...rest] = args;
+      if(type === "beforeunload") {
+        return original.apply(this, [
+          type,
+          (...a) => {
+            if(beforeUnloadEnabled)
+              listener(...a);
+          },
+          ...rest,
+        ]);
+      }
+      else
+        return original.apply(this, args);
+    };
+  })(window.__proto__.addEventListener);
+
+  getFeatures().then(feats => {
+    if(feats.disableBeforeUnloadPopup)
+      disableBeforeUnload();
+  });
+
+  // (function(original) {
+  //   window.__proto__.removeEventListener = function(type, listener, useCapture) {
+  //     if(evtNames.includes(type)){
+  //       console.log("------> removeEventListener " + type, listener, useCapture);
+  //     }
+
+  //     return original.apply(this, arguments);
+  //   };
+  // })(window.__proto__.removeEventListener);
+}

+ 11 - 0
src/types.d.ts

@@ -1,3 +1,12 @@
+// generic shim so TS doesn't complain *too* much
+declare global {
+  interface Window {
+    __proto__: {
+      addEventListener: (evt: string, listener: () => unknown, capture?: boolean) => void;
+    }
+  }
+}
+
 /** Which domain this script is currently running on */
 export type Domain = "yt" | "ytm";
 
@@ -15,6 +24,8 @@ export interface FeatureConfig {
     ctrl: boolean;
     meta: boolean;
   };
+  /** Whether to completely disable the popup that sometimes appears before leaving the site */
+  disableBeforeUnloadPopup: boolean;
   /** TODO: Make it so middle clicking a song to open it in a new tab (through thumbnail and song title) is easier */
   anchorImprovements: boolean;