Bläddra i källkod

ref: extract site events into own file

Sv443 1 år sedan
förälder
incheckning
36f51ad237
5 ändrade filer med 110 tillägg och 106 borttagningar
  1. 1 1
      src/dev/discoveries.md
  2. 104 0
      src/events.ts
  3. 2 1
      src/features/layout.ts
  4. 2 1
      src/index.ts
  5. 1 103
      src/utils.ts

+ 1 - 1
src/dev/discoveries.md

@@ -2,7 +2,7 @@
 
 ### The problem with userscripts and SPAs:
 YTM is an SPA (single page application), meaning navigating to a different part of the site doesn't trigger the website, and by extension userscripts, to entirely reload like traditional redirects on MPAs (multi-page applications).  
-This means userscripts like BetterYTM rely on detecting changes in the DOM using something like the [MutationObserver API.](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)  
+This means userscripts like BetterYTM rely on detecting changes in the DOM using something like the [MutationObserver API](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) (see [`src/events.ts`](../events.ts)).  
 This causes a LOT of headaches (race conditions, detecting navigation, state consistency and more) but it's the only option as far as I'm aware.
 
 <br>

+ 104 - 0
src/events.ts

@@ -0,0 +1,104 @@
+import { Event as EventParam, EventEmitter, EventHandler } from "@billjs/event-emitter";
+import { error, info } from "./utils";
+
+export interface SiteEvents extends EventEmitter {
+  /** Emitted whenever child nodes are added to or removed from the song queue */
+  on(event: "queueChanged", listener: EventHandler): boolean;
+  /** Emitted whenever child nodes are added to or removed from the autoplay queue underneath the song queue */
+  on(event: "autoplayQueueChanged", listener: EventHandler): boolean;
+  /** Emitted whenever carousel shelf containers are added or removed from their parent container */
+  on(event: "carouselShelvesChanged", listener: EventHandler): boolean;
+  /** Emitted once the home page is filled with content */
+  on(event: "homePageLoaded", listener: EventHandler): boolean;
+}
+
+/** EventEmitter instance that is used to detect changes to the site */
+export const siteEvents = new EventEmitter() as SiteEvents;
+
+/**
+ * Returns the data of an event from the `@billjs/event-emitter` library.
+ * This function is used as a shorthand to extract the data and assert it with the type passed in `<T>`
+ * @param evt Event object from the `.on()` or `.once()` method
+ * @template T Type of the data passed by `.fire(type: string, data: T)`
+ */
+export function getEvtData<T>(evt: EventParam): T {
+  return evt.data as T;
+}
+
+let observers: MutationObserver[] = [];
+
+/** Disconnects and deletes all observers. Run `initSiteEvents()` again to create new ones. */
+export function removeAllObservers() {
+  observers.forEach((observer) => observer.disconnect());
+  observers = [];
+}
+
+/** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
+export async function initSiteEvents() {
+  try {
+    //#SECTION queue
+    // the queue container always exists so it doesn't need the extra init function
+    const queueObs = new MutationObserver(([ { addedNodes, removedNodes, target } ]) => {
+      if(addedNodes.length > 0 || removedNodes.length > 0) {
+        info(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
+        siteEvents.fire("queueChanged", target);
+      }
+    });
+
+    // only observe added or removed elements
+    queueObs.observe(document.querySelector(".side-panel.modular #contents.ytmusic-player-queue")!, {
+      childList: true,
+    });
+
+    //#SECTION home page observers
+    initHomeObservers();
+
+    info("Successfully initialized SiteEvents observers");
+
+    observers = [
+      queueObs,
+    ];
+  }
+  catch(err) {
+    error("Couldn't initialize SiteEvents observers due to an error:\n", err);
+  }
+}
+
+/**
+ * The home page might not exist yet if the site was accessed through any path like /watch directly.
+ * This function will keep waiting for when the home page exists, then create the necessary MutationObservers.
+ */
+async function initHomeObservers() {
+  let interval: NodeJS.Timer | undefined;
+
+  // hidden="" attribute is only present if the content of the page doesn't exist yet
+  // so this pauses execution until that attribute is removed
+  if(document.querySelector("ytmusic-browse-response#browse-page")?.hasAttribute("hidden")) {
+    await new Promise<void>((res) => {
+      interval = setInterval(() => {
+        if(!document.querySelector("ytmusic-browse-response#browse-page")?.hasAttribute("hidden")) {
+          clearInterval(interval);
+          res();
+        }
+      }, 50);
+    });
+  }
+
+  siteEvents.fire("homePageLoaded");
+
+  info("Initialized home page observers");
+
+  //#SECTION carousel shelves
+  const shelfContainerObs = new MutationObserver(([ { addedNodes, removedNodes } ]) => {
+    if(addedNodes.length > 0 || removedNodes.length > 0) {
+      info("Detected carousel shelf container change - added nodes:", addedNodes.length, "- removed nodes:", removedNodes.length);
+      siteEvents.fire("carouselShelvesChanged", { addedNodes, removedNodes });
+    }
+  });
+
+  shelfContainerObs.observe(document.querySelector("#contents.ytmusic-section-list-renderer")!, {
+    childList: true,
+  });
+
+  observers = observers.concat([ shelfContainerObs ]);
+}

+ 2 - 1
src/features/layout.ts

@@ -1,6 +1,7 @@
 import { scriptInfo, triesInterval, triesLimit } from "../constants";
 import { getFeatures } from "../config";
-import { addGlobalStyle, autoPlural, error, getAssetUrl, getEvtData, insertAfter, log, openInNewTab, siteEvents } from "../utils";
+import { addGlobalStyle, autoPlural, error, getAssetUrl, insertAfter, log, openInNewTab } from "../utils";
+import { getEvtData, siteEvents } from "../events";
 import type { FeatureConfig } from "../types";
 import { openMenu } from "./menu/menu_old";
 import "./layout.css";

+ 2 - 1
src/index.ts

@@ -1,6 +1,7 @@
 import { loadFeatureConf } from "./config";
 import { logLevel, scriptInfo } from "./constants";
-import { addGlobalStyle, error, getDomain, initSiteEvents, log, setLogLevel } from "./utils";
+import { addGlobalStyle, error, getDomain, log, setLogLevel } from "./utils";
+import { initSiteEvents } from "./events";
 import {
   // layout
   initQueueButtons, addWatermark,

+ 1 - 103
src/utils.ts

@@ -1,4 +1,3 @@
-import { Event as EventParam, EventEmitter, EventHandler } from "@billjs/event-emitter";
 import { branch, scriptInfo } from "./constants";
 import type { Domain, LogLevel } from "./types";
 
@@ -177,7 +176,7 @@ export function openInNewTab(href: string) {
   });
   document.body.appendChild(openElem);
   openElem.click();
-  // just to be safe
+  // timeout just to be safe
   setTimeout(() => openElem.remove(), 200);
 }
 
@@ -222,104 +221,3 @@ export function addGlobalStyle(style: string, ref?: string) {
 
   log(`Inserted global style with ref '${ref}':`, styleElem);
 }
-
-//#MARKER site events
-
-export interface SiteEvents extends EventEmitter {
-  /** Emitted whenever child nodes are added to or removed from the song queue */
-  on(event: "queueChanged", listener: EventHandler): boolean;
-  /** Emitted whenever carousel shelf containers are added or removed from their parent container */
-  on(event: "carouselShelvesChanged", listener: EventHandler): boolean;
-  /** Emitted once the home page is filled with content */
-  on(event: "homePageLoaded", listener: EventHandler): boolean;
-}
-
-export const siteEvents = new EventEmitter() as SiteEvents;
-
-/**
- * Returns the data of an event from the `@billjs/event-emitter` library.  
- * This function is used to assert the type passed in `<T>`
- * @param evt Event object from the `.on()` or `.once()` method
- * @template T Type of the data passed by `.fire(type: string, data: T)`
- */
-export function getEvtData<T>(evt: EventParam): T {
-  return evt.data as T;
-}
-
-let observers: MutationObserver[] = [];
-
-/** Disconnects and deletes all observers. Run `initSiteEvents()` again to create new ones. */
-export function removeAllObservers() {
-  observers.forEach((observer) => observer.disconnect());
-  observers = [];
-}
-
-/** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
-export async function initSiteEvents() {
-  try {
-    //#SECTION queue
-    // the queue container always exists so it doesn't need the extra init function
-    const queueObs = new MutationObserver(([ { addedNodes, removedNodes, target } ]) => {
-      if(addedNodes.length > 0 || removedNodes.length > 0) {
-        info(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
-        siteEvents.fire("queueChanged", target);
-      }
-    });
-    // only observe added or removed elements
-    queueObs.observe(document.querySelector(".side-panel.modular #contents.ytmusic-player-queue")!, {
-      childList: true,
-    });
-
-    //#SECTION home page observers
-    initHomeObservers();
-
-    info("Successfully initialized SiteEvents observers");
-
-    observers = [
-      queueObs,
-    ];
-  }
-  catch(err) {
-    error("Couldn't initialize SiteEvents observers due to an error:\n", err);
-  }
-}
-
-/**
- * The home page might not exist yet if the site was accessed through any path like /watch directly.  
- * This function will keep waiting for when the home page exists, then create the necessary MutationObservers.
- */
-async function initHomeObservers() {
-  let interval: NodeJS.Timer | undefined;
-
-  // hidden="" attribute is only present if the content of the page doesn't exist yet
-  // so this resolves only once that attribute is removed
-  if(document.querySelector("ytmusic-browse-response#browse-page")?.hasAttribute("hidden")) {
-    await new Promise<void>((res) => {
-      interval = setInterval(() => {
-        if(!document.querySelector("ytmusic-browse-response#browse-page")?.hasAttribute("hidden")) {
-          info("found home page");
-          res();
-        }
-      }, 50);
-    });
-  }
-  interval && clearInterval(interval);
-
-  siteEvents.fire("homePageLoaded");
-
-  info("Initialized home page observers");
-
-  //#SECTION carousel shelves
-  const shelfContainerObs = new MutationObserver(([ { addedNodes, removedNodes } ]) => {
-    if(addedNodes.length > 0 || removedNodes.length > 0) {
-      info("Detected carousel shelf container change - added nodes:", addedNodes.length, "- removed nodes:", removedNodes.length);
-      siteEvents.fire("carouselShelvesChanged", { addedNodes, removedNodes });
-    }
-  });
-
-  shelfContainerObs.observe(document.querySelector("#contents.ytmusic-section-list-renderer")!, {
-    childList: true,
-  });
-
-  observers = observers.concat([ shelfContainerObs ]);
-}