Browse Source

ref!: rename globservers and add back ytMasthead

Sv443 10 months ago
parent
commit
a80f39822a
3 changed files with 110 additions and 67 deletions
  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:**
 - **Internal Changes:**
   - Removed `compareVersions()` and `compareVersionArrays()` in favor of including the [`compare-versions`](https://npmjs.com/package/compare-versions) library
   - 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
   - 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:
   - Added new components:
     -  `createLongBtn()` to create a button with an icon and text (works either as normal or as a toggle button)  
     -  `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.
       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)
     - `createRipple()` to create a click ripple animation effect on a given element (experimental)
   - Added new SelectorObserver instance `browseResponse` for pages like `/channel/{id}`
   - Added new SelectorObserver instance `browseResponse` for pages like `/channel/{id}`
   - Renamed event `bytm:initPlugins` to `bytm:registerPlugins` to be more consistent
   - 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
   - Added events
     - `bytm:featureInitStarted` - emitted when the feature initialization process starts
     - `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
     - `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:  
 > 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:  
 > 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.
 > - `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)
 > - `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
 > ```ts
 > // wait for the observers to exist
 > // wait for the observers to exist
 > unsafeWindow.addEventListener("bytm:observersReady", () => {
 > 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 { 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 */
 /** Names of all available Observer instances across all sites */
 export type ObserverName = SharedObserverName | YTMObserverName | YTObserverName;
 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);
 export type ObserverNameByDomain<TDomain extends Domain> = SharedObserverName | (TDomain extends "ytm" ? YTMObserverName : YTObserverName);
 
 
 // Shared between YTM and YT
 // Shared between YTM and YT
@@ -29,23 +36,52 @@ export type YTMObserverName =
 
 
 // YT only
 // YT only
 export type YTObserverName =
 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 */
 /** Options that are applied to every SelectorObserver instance */
 const defaultObserverOptions: SelectorObserverOptions = {
 const defaultObserverOptions: SelectorObserverOptions = {
   disableOnNoListeners: false,
   disableOnNoListeners: false,
   enableOnAddListener: false,
   enableOnAddListener: false,
-  defaultDebounce: 100,
+  defaultDebounce: 150,
   defaultDebounceEdge: "rising",
   defaultDebounceEdge: "rising",
 };
 };
 
 
 /** Global SelectorObserver instances usable throughout the script for improved performance */
 /** Global SelectorObserver instances usable throughout the script for improved performance */
 export const globservers = {} as Record<ObserverName, SelectorObserver>;
 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 */
 /** Call after DOM load to initialize all SelectorObserver instances */
 export function initObservers() {
 export function initObservers() {
   try {
   try {
@@ -53,6 +89,7 @@ export function initObservers() {
 
 
     //#region body
     //#region body
     // -> the entire <body> element - use sparingly due to performance impacts!
     // -> the entire <body> element - use sparingly due to performance impacts!
+    //    enabled immediately
     globservers.body = new SelectorObserver(document.body, {
     globservers.body = new SelectorObserver(document.body, {
       ...defaultObserverOptions,
       ...defaultObserverOptions,
       defaultDebounce: 150,
       defaultDebounce: 150,
@@ -66,7 +103,8 @@ export function initObservers() {
       //#region YTM
       //#region YTM
 
 
       //#region browseResponse
       //#region browseResponse
-      // -> for example the /channel/UC... page
+      // -> for example the /channel/UC... page#
+      //    enabled by "body"
       const browseResponseSelector = "ytmusic-browse-response";
       const browseResponseSelector = "ytmusic-browse-response";
       globservers.browseResponse = new SelectorObserver(browseResponseSelector, {
       globservers.browseResponse = new SelectorObserver(browseResponseSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -79,6 +117,7 @@ export function initObservers() {
 
 
       //#region navBar
       //#region navBar
       // -> the navigation / title bar at the top of the page
       // -> the navigation / title bar at the top of the page
+      //    enabled by "body"
       const navBarSelector = "ytmusic-nav-bar";
       const navBarSelector = "ytmusic-nav-bar";
       globservers.navBar = new SelectorObserver(navBarSelector, {
       globservers.navBar = new SelectorObserver(navBarSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -91,6 +130,7 @@ export function initObservers() {
 
 
       //#region mainPanel
       //#region mainPanel
       // -> the main content panel - includes things like the video element
       // -> the main content panel - includes things like the video element
+      //    enabled by "body"
       const mainPanelSelector = "ytmusic-player-page #main-panel";
       const mainPanelSelector = "ytmusic-player-page #main-panel";
       globservers.mainPanel = new SelectorObserver(mainPanelSelector, {
       globservers.mainPanel = new SelectorObserver(mainPanelSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -103,6 +143,7 @@ export function initObservers() {
 
 
       //#region sideBar
       //#region sideBar
       // -> the sidebar on the left side of the page
       // -> the sidebar on the left side of the page
+      //    enabled by "body"
       const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
       const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
       globservers.sideBar = new SelectorObserver(sidebarSelector, {
       globservers.sideBar = new SelectorObserver(sidebarSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -115,6 +156,7 @@ export function initObservers() {
 
 
       //#region sideBarMini
       //#region sideBarMini
       // -> the minimized sidebar on the left side of the page
       // -> the minimized sidebar on the left side of the page
+      //    enabled by "body"
       const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
       const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
       globservers.sideBarMini = new SelectorObserver(sideBarMiniSelector, {
       globservers.sideBarMini = new SelectorObserver(sideBarMiniSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -127,6 +169,7 @@ export function initObservers() {
 
 
       //#region sidePanel
       //#region sidePanel
       // -> the side panel on the right side of the /watch page
       // -> the side panel on the right side of the /watch page
+      //    enabled by "body"
       const sidePanelSelector = "#side-panel";
       const sidePanelSelector = "#side-panel";
       globservers.sidePanel = new SelectorObserver(sidePanelSelector, {
       globservers.sidePanel = new SelectorObserver(sidePanelSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -139,6 +182,7 @@ export function initObservers() {
 
 
       //#region playerBar
       //#region playerBar
       // -> media controls bar at the bottom of the page
       // -> media controls bar at the bottom of the page
+      //    enabled by "body"
       const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
       const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
       globservers.playerBar = new SelectorObserver(playerBarSelector, {
       globservers.playerBar = new SelectorObserver(playerBarSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -153,6 +197,7 @@ export function initObservers() {
 
 
       //#region playerBarInfo
       //#region playerBarInfo
       // -> song title, artist, album, etc. inside the player bar
       // -> song title, artist, album, etc. inside the player bar
+      //    enabled by "playerBar"
       const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
       const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
       globservers.playerBarInfo = new SelectorObserver(playerBarInfoSelector, {
       globservers.playerBarInfo = new SelectorObserver(playerBarInfoSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -166,6 +211,7 @@ export function initObservers() {
 
 
       //#region playerBarMiddleButtons
       //#region playerBarMiddleButtons
       // -> the buttons inside the player bar (like, dislike, lyrics, etc.)
       // -> the buttons inside the player bar (like, dislike, lyrics, etc.)
+      //    enabled by "playerBar"
       const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
       const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
       globservers.playerBarMiddleButtons = new SelectorObserver(playerBarMiddleButtonsSelector, {
       globservers.playerBarMiddleButtons = new SelectorObserver(playerBarMiddleButtonsSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -178,6 +224,7 @@ export function initObservers() {
 
 
       //#region playerBarRightControls
       //#region playerBarRightControls
       // -> the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
       // -> the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
+      //    enabled by "playerBar"
       const playerBarRightControls = "#right-controls";
       const playerBarRightControls = "#right-controls";
       globservers.playerBarRightControls = new SelectorObserver(playerBarRightControls, {
       globservers.playerBarRightControls = new SelectorObserver(playerBarRightControls, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -190,6 +237,7 @@ export function initObservers() {
 
 
       //#region popupContainer
       //#region popupContainer
       // -> the container for popups (e.g. the queue popup)
       // -> the container for popups (e.g. the queue popup)
+      //    enabled by "body"
       const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
       const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
       globservers.popupContainer = new SelectorObserver(popupContainerSelector, {
       globservers.popupContainer = new SelectorObserver(popupContainerSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -207,6 +255,7 @@ export function initObservers() {
 
 
       //#region ytGuide
       //#region ytGuide
       // -> the left sidebar menu
       // -> the left sidebar menu
+      //    enabled by "body"
       const ytGuideSelector = "#content tp-yt-app-drawer#guide #guide-inner-content";
       const ytGuideSelector = "#content tp-yt-app-drawer#guide #guide-inner-content";
       globservers.ytGuide = new SelectorObserver(ytGuideSelector, {
       globservers.ytGuide = new SelectorObserver(ytGuideSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -219,6 +268,7 @@ export function initObservers() {
 
 
       //#region ytdBrowse
       //#region ytdBrowse
       // -> channel pages for example
       // -> channel pages for example
+      //    enabled by "body"
       const ytdBrowseSelector = "ytd-app ytd-page-manager ytd-browse";
       const ytdBrowseSelector = "ytd-app ytd-page-manager ytd-browse";
       globservers.ytdBrowse = new SelectorObserver(ytdBrowseSelector, {
       globservers.ytdBrowse = new SelectorObserver(ytdBrowseSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -231,6 +281,7 @@ export function initObservers() {
 
 
       //#region ytChannelHeader
       //#region ytChannelHeader
       // -> header of a channel page
       // -> header of a channel page
+      //    enabled by "ytdBrowse"
       const ytChannelHeaderSelector = "#header tp-yt-app-header #channel-header";
       const ytChannelHeaderSelector = "#header tp-yt-app-header #channel-header";
       globservers.ytChannelHeader = new SelectorObserver(ytChannelHeaderSelector, {
       globservers.ytChannelHeader = new SelectorObserver(ytChannelHeaderSelector, {
         ...defaultObserverOptions,
         ...defaultObserverOptions,
@@ -241,41 +292,44 @@ export function initObservers() {
         listener: () => globservers.ytChannelHeader.enable(),
         listener: () => globservers.ytChannelHeader.enable(),
       });
       });
 
 
-      //#region watchFlexy
+      //#region ytWatchFlexy
       // -> the main content of the /watch page
       // -> 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,
         ...defaultObserverOptions,
         subtree: true,
         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)
       // -> 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,
         ...defaultObserverOptions,
         subtree: true,
         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
       // -> 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);
     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);
-}