Explorar o código

ref!: rename globservers and add back ytMasthead

Sv443 hai 10 meses
pai
achega
a80f39822a
Modificáronse 3 ficheiros con 110 adicións e 67 borrados
  1. 9 3
      changelog.md
  2. 16 9
      contributing.md
  3. 85 55
      src/observers.ts

+ 9 - 3
changelog.md

@@ -19,9 +19,14 @@
 - **Internal Changes:**
   - Removed `compareVersions()` and `compareVersionArrays()` in favor of including the [`compare-versions`](https://npmjs.com/package/compare-versions) library
   - Added advanced feature to change the startup timeout
-  - A blue logo is shown instead of the red BetterYTM logo when the script was compiled in development (preview) mode
-  - In development mode, missing configuration keys will now be set to their default value instead of potentially breaking the script
-- **Plugin Changes:**
+  - A blue logo is shown instead of the red BetterYTM logo when the script was compiled in development / preview mode
+  - In dev/preview mode, missing configuration keys will now be set to their default value instead of potentially breaking the script
+  - SelectorObserver changes:
+    - Added `ytMasthead` instance for the title bar on YT
+    - Renamed all YT-specific instances to have the `yt` prefix
+      - From `watchFlexy` to `ytWatchFlexy`
+      - From `watchMetadata` to `ytWatchMetadata`
+- **Plugin Interface Changes:**
   - Added new components:
     -  `createLongBtn()` to create a button with an icon and text (works either as normal or as a toggle button)  
       The design follows that of the subscribe button on YTM's channel pages, but the consistent class names make it easy to style it differently.
@@ -30,6 +35,7 @@
     - `createRipple()` to create a click ripple animation effect on a given element (experimental)
   - Added new SelectorObserver instance `browseResponse` for pages like `/channel/{id}`
   - Renamed event `bytm:initPlugins` to `bytm:registerPlugins` to be more consistent
+  - Added library `compare-versions` to the plugin interface at `unsafeWindow.BYTM.compareVersions` for easier plugin version comparison
   - Added events
     - `bytm:featureInitStarted` - emitted when the feature initialization process starts
     - `bytm:featureInitialized` - emitted every time a feature has been initialized and is passed the feature's identifier string

+ 16 - 9
contributing.md

@@ -524,12 +524,19 @@ The usage and example blocks on each are written in TypeScript but can be used i
 > ```
 >   
 > Description:  
-> Adds a listener to the specified SelectorObserver instance that gets called when the element(s) behind the passed selector change.  
-> These instances are created by BetterYTM to observe the DOM for changes.  
-> See the [UserUtils SelectorObserver documentation](https://github.com/Sv443-Network/UserUtils#selectorobserver) for more info.  
+> Adds a listener to the specified SelectorObserver instance that gets called when the element/s behind the passed selector is/are found.  
+> They are immediately checked for and then checked again whenever the part of the DOM tree changes (elements get added or removed) that is observed by that specific SelectorObserver.  
+>   
+> The instances are chained together in a way that the least specific observer is the parent of the more specific ones.  
+> This is done to limit the amount of checks that need to be run, especially on pages with a lot of dynamic content and if `continuous` listeners are used.  
+> See the [UserUtils SelectorObserver documentation](https://github.com/Sv443-Network/UserUtils#selectorobserver) for more info and example code.  
+>   
+> ⚠️ Due to this chained architecture, the selector you pass can only start with an element that is a child of the observer's base element.  
+> If you provide a selector that starts higher up or directly on the base element, the listener will never be called.  
+> You can check which observer has which base element in the file [`src/observers.ts`](src/observers.ts)  
 >   
 > Arguments:  
-> - `observerName` - The name of the SelectorObserver instance to add the listener to. You can find all available instances and which parent element they observe in the file [`src/observers.ts`](src/observers.ts).
+> - `observerName` - The name of the SelectorObserver instance to add the listener to. You can find all available instances and which base element they observe in the file [`src/observers.ts`](src/observers.ts)
 > - `selector` - The CSS selector to observe for changes.
 > - `options` - The options for the listener. See the [UserUtils SelectorObserver documentation](https://github.com/Sv443-Network/UserUtils#selectorobserver)
 >   
@@ -538,11 +545,11 @@ The usage and example blocks on each are written in TypeScript but can be used i
 > ```ts
 > // wait for the observers to exist
 > unsafeWindow.addEventListener("bytm:observersReady", () => {
->   // use the "lowest" possible SelectorObserver (playerBar)
->   // and check if the lyrics button gets added or removed
->   unsafeWindow.BYTM.addSelectorListener("playerBar", "#betterytm-lyrics-button", {
->     listener: (elem) => {
->       console.log("The BYTM lyrics button changed");
+>   // use the "lowest" possible SelectorObserver (playerBar) to prevent unnecessary checks
+>   // and call the listener as soon as the passed selector is found in the DOM
+>   unsafeWindow.BYTM.addSelectorListener<HTMLAnchorElement>("playerBar", "#bytm-player-bar-lyrics-btn", {
+>     listener: (lyricsBtnElem) => {
+>       console.log("The player bar lyrics button was added or removed:", lyricsBtnElem);
 >     },
 >   });
 > });

+ 85 - 55
src/observers.ts

@@ -1,12 +1,19 @@
 import { SelectorListenerOptions, SelectorObserver, SelectorObserverOptions } from "@sv443-network/userutils";
-import { emitInterface } from "./interface";
-import { error, getDomain } from "./utils";
-import type { Domain } from "./types";
+import { emitInterface } from "./interface.js";
+import { error, getDomain } from "./utils/index.js";
+import type { Domain } from "./types.js";
+
+// >> If you came here looking for which observer to use, start out by looking at the types `YTMObserverName` and `YTObserverName`
+// >> Once you found a fitting observer, go to the `initObservers()` function and search for `observerName = new SelectorObserver`
+// >> Just above that line, you'll find the selector to that observer's base element. Make sure all your selectors start **below** that element!
+
+
+//#region types
 
 /** Names of all available Observer instances across all sites */
 export type ObserverName = SharedObserverName | YTMObserverName | YTObserverName;
 
-/** Observer names available to each site */
+/** Observer names available to the site passed in the `TDomain` generic */
 export type ObserverNameByDomain<TDomain extends Domain> = SharedObserverName | (TDomain extends "ytm" ? YTMObserverName : YTObserverName);
 
 // Shared between YTM and YT
@@ -29,23 +36,52 @@ export type YTMObserverName =
 
 // YT only
 export type YTObserverName =
-  | "ytGuide"         // the left sidebar menu
-  | "ytdBrowse"       // channel pages for example
-  | "ytChannelHeader" // header of a channel page
-  | "watchFlexy"      // the main content of the /watch page
-  | "watchMetadata";  // the metadata section of the /watch page
+  | "ytMasthead"       // the masthead (title bar) at the top of the page
+  | "ytGuide"          // the left sidebar menu
+  | "ytdBrowse"        // channel pages for example
+  | "ytChannelHeader"  // header of a channel page
+  | "ytWatchFlexy"     // the main content of the /watch page
+  | "ytWatchMetadata"; // the metadata section of the /watch page
+
+//#region globals
 
 /** Options that are applied to every SelectorObserver instance */
 const defaultObserverOptions: SelectorObserverOptions = {
   disableOnNoListeners: false,
   enableOnAddListener: false,
-  defaultDebounce: 100,
+  defaultDebounce: 150,
   defaultDebounceEdge: "rising",
 };
 
 /** Global SelectorObserver instances usable throughout the script for improved performance */
 export const globservers = {} as Record<ObserverName, SelectorObserver>;
 
+//#region add listener func
+
+/**
+ * Interface function for adding listeners to the {@linkcode globservers}  
+ * @param selector Relative to the observer's root element, so the selector can only start at of the root element's children at the earliest!
+ * @param options Options for the listener
+ * @template TElem The type of the element that the listener will be attached to. If set to `0`, the default type `HTMLElement` will be used.
+ * @template TDomain This restricts which observers are available with the current domain
+ */
+export function addSelectorListener<
+  TElem extends HTMLElement | 0 = HTMLElement,
+  TDomain extends Domain = "ytm"
+> (
+  observerName: ObserverNameByDomain<TDomain>,
+  selector: string,
+  options: SelectorListenerOptions<
+    TElem extends 0
+      ? HTMLElement
+      : TElem
+  >,
+) {
+  globservers[observerName].addListener(selector, options);
+}
+
+//#region init
+
 /** Call after DOM load to initialize all SelectorObserver instances */
 export function initObservers() {
   try {
@@ -53,6 +89,7 @@ export function initObservers() {
 
     //#region body
     // -> the entire <body> element - use sparingly due to performance impacts!
+    //    enabled immediately
     globservers.body = new SelectorObserver(document.body, {
       ...defaultObserverOptions,
       defaultDebounce: 150,
@@ -66,7 +103,8 @@ export function initObservers() {
       //#region YTM
 
       //#region browseResponse
-      // -> for example the /channel/UC... page
+      // -> for example the /channel/UC... page#
+      //    enabled by "body"
       const browseResponseSelector = "ytmusic-browse-response";
       globservers.browseResponse = new SelectorObserver(browseResponseSelector, {
         ...defaultObserverOptions,
@@ -79,6 +117,7 @@ export function initObservers() {
 
       //#region navBar
       // -> the navigation / title bar at the top of the page
+      //    enabled by "body"
       const navBarSelector = "ytmusic-nav-bar";
       globservers.navBar = new SelectorObserver(navBarSelector, {
         ...defaultObserverOptions,
@@ -91,6 +130,7 @@ export function initObservers() {
 
       //#region mainPanel
       // -> the main content panel - includes things like the video element
+      //    enabled by "body"
       const mainPanelSelector = "ytmusic-player-page #main-panel";
       globservers.mainPanel = new SelectorObserver(mainPanelSelector, {
         ...defaultObserverOptions,
@@ -103,6 +143,7 @@ export function initObservers() {
 
       //#region sideBar
       // -> the sidebar on the left side of the page
+      //    enabled by "body"
       const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
       globservers.sideBar = new SelectorObserver(sidebarSelector, {
         ...defaultObserverOptions,
@@ -115,6 +156,7 @@ export function initObservers() {
 
       //#region sideBarMini
       // -> the minimized sidebar on the left side of the page
+      //    enabled by "body"
       const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
       globservers.sideBarMini = new SelectorObserver(sideBarMiniSelector, {
         ...defaultObserverOptions,
@@ -127,6 +169,7 @@ export function initObservers() {
 
       //#region sidePanel
       // -> the side panel on the right side of the /watch page
+      //    enabled by "body"
       const sidePanelSelector = "#side-panel";
       globservers.sidePanel = new SelectorObserver(sidePanelSelector, {
         ...defaultObserverOptions,
@@ -139,6 +182,7 @@ export function initObservers() {
 
       //#region playerBar
       // -> media controls bar at the bottom of the page
+      //    enabled by "body"
       const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
       globservers.playerBar = new SelectorObserver(playerBarSelector, {
         ...defaultObserverOptions,
@@ -153,6 +197,7 @@ export function initObservers() {
 
       //#region playerBarInfo
       // -> song title, artist, album, etc. inside the player bar
+      //    enabled by "playerBar"
       const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
       globservers.playerBarInfo = new SelectorObserver(playerBarInfoSelector, {
         ...defaultObserverOptions,
@@ -166,6 +211,7 @@ export function initObservers() {
 
       //#region playerBarMiddleButtons
       // -> the buttons inside the player bar (like, dislike, lyrics, etc.)
+      //    enabled by "playerBar"
       const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
       globservers.playerBarMiddleButtons = new SelectorObserver(playerBarMiddleButtonsSelector, {
         ...defaultObserverOptions,
@@ -178,6 +224,7 @@ export function initObservers() {
 
       //#region playerBarRightControls
       // -> the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
+      //    enabled by "playerBar"
       const playerBarRightControls = "#right-controls";
       globservers.playerBarRightControls = new SelectorObserver(playerBarRightControls, {
         ...defaultObserverOptions,
@@ -190,6 +237,7 @@ export function initObservers() {
 
       //#region popupContainer
       // -> the container for popups (e.g. the queue popup)
+      //    enabled by "body"
       const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
       globservers.popupContainer = new SelectorObserver(popupContainerSelector, {
         ...defaultObserverOptions,
@@ -207,6 +255,7 @@ export function initObservers() {
 
       //#region ytGuide
       // -> the left sidebar menu
+      //    enabled by "body"
       const ytGuideSelector = "#content tp-yt-app-drawer#guide #guide-inner-content";
       globservers.ytGuide = new SelectorObserver(ytGuideSelector, {
         ...defaultObserverOptions,
@@ -219,6 +268,7 @@ export function initObservers() {
 
       //#region ytdBrowse
       // -> channel pages for example
+      //    enabled by "body"
       const ytdBrowseSelector = "ytd-app ytd-page-manager ytd-browse";
       globservers.ytdBrowse = new SelectorObserver(ytdBrowseSelector, {
         ...defaultObserverOptions,
@@ -231,6 +281,7 @@ export function initObservers() {
 
       //#region ytChannelHeader
       // -> header of a channel page
+      //    enabled by "ytdBrowse"
       const ytChannelHeaderSelector = "#header tp-yt-app-header #channel-header";
       globservers.ytChannelHeader = new SelectorObserver(ytChannelHeaderSelector, {
         ...defaultObserverOptions,
@@ -241,41 +292,44 @@ export function initObservers() {
         listener: () => globservers.ytChannelHeader.enable(),
       });
 
-      //#region watchFlexy
+      //#region ytWatchFlexy
       // -> the main content of the /watch page
-      const watchFlexySelector = "ytd-app ytd-watch-flexy";
-      globservers.watchFlexy = new SelectorObserver(watchFlexySelector, {
+      //    enabled by "body"
+      const ytWatchFlexySelector = "ytd-app ytd-watch-flexy";
+      globservers.ytWatchFlexy = new SelectorObserver(ytWatchFlexySelector, {
         ...defaultObserverOptions,
         subtree: true,
       });
 
-      globservers.body.addListener(watchFlexySelector, {
-        listener: () => globservers.watchFlexy.enable(),
+      globservers.body.addListener(ytWatchFlexySelector, {
+        listener: () => globservers.ytWatchFlexy.enable(),
       });
 
-      //#region watchMetadata
+      //#region ytWatchMetadata
       // -> the metadata section of the /watch page (title, channel, views, description, buttons, etc. but not comments)
-      const watchMetadataSelector = "#columns #primary-inner ytd-watch-metadata";
-      globservers.watchMetadata = new SelectorObserver(watchMetadataSelector, {
+      //    enabled by "ytWatchFlexy"
+      const ytWatchMetadataSelector = "#columns #primary-inner ytd-watch-metadata";
+      globservers.ytWatchMetadata = new SelectorObserver(ytWatchMetadataSelector, {
         ...defaultObserverOptions,
         subtree: true,
       });
 
-      globservers.watchFlexy.addListener(watchMetadataSelector, {
-        listener: () => globservers.watchMetadata.enable(),
+      globservers.ytWatchFlexy.addListener(ytWatchMetadataSelector, {
+        listener: () => globservers.ytWatchMetadata.enable(),
       });
 
-      // //#region ytMasthead
+      //#region ytMasthead
       // -> the masthead (title bar) at the top of the page
-      // const mastheadSelector = "#content ytd-masthead#masthead";
-      // globservers.ytMasthead = new SelectorObserver(mastheadSelector, {
-      //   ...defaultObserverOptions,
-      //   subtree: true,
-      // });
-
-      // globservers.body.addListener(mastheadSelector, {
-      //   listener: () => globservers.ytMasthead.enable(),
-      // });
+      //    enabled by "body"
+      const mastheadSelector = "#content ytd-masthead#masthead";
+      globservers.ytMasthead = new SelectorObserver(mastheadSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.body.addListener(mastheadSelector, {
+        listener: () => globservers.ytMasthead.enable(),
+      });
     }
     }
 
@@ -287,27 +341,3 @@ export function initObservers() {
     error("Failed to initialize observers:", err);
   }
 }
-
-//#region add listener func
-
-/**
- * Interface function for adding listeners to the {@linkcode globservers}  
- * @param selector Relative to the observer's root element, so the selector can only start at of the root element's children at the earliest!
- * @param options Options for the listener
- * @template TElem The type of the element that the listener will be attached to. If set to `0`, the type HTMLElement will be used.
- * @template TDomain This restricts which observers are available with the current domain
- */
-export function addSelectorListener<
-  TElem extends HTMLElement | 0 = HTMLElement,
-  TDomain extends Domain = "ytm"
->(
-  observerName: ObserverNameByDomain<TDomain>,
-  selector: string,
-  options: SelectorListenerOptions<
-    TElem extends 0
-      ? HTMLElement
-      : TElem
-  >
-){
-  globservers[observerName].addListener(selector, options);
-}