observers.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import { SelectorListenerOptions, SelectorObserver, SelectorObserverOptions } from "@sv443-network/userutils";
  2. import { emitInterface } from "./interface";
  3. import { error, getDomain } from "./utils";
  4. import type { Domain } from "./types";
  5. /** Names of all available Observer instances across all sites */
  6. export type ObserverName = SharedObserverName | YTMObserverName | YTObserverName;
  7. /** Observer names available to each site */
  8. export type ObserverNameByDomain<TDomain extends Domain> = SharedObserverName | (TDomain extends "ytm" ? YTMObserverName : YTObserverName);
  9. // both YTM and YT
  10. export type SharedObserverName =
  11. | "body";
  12. // YTM only
  13. export type YTMObserverName =
  14. | "navBar"
  15. | "mainPanel"
  16. | "sideBar"
  17. | "sideBarMini"
  18. | "sidePanel"
  19. | "playerBar"
  20. | "playerBarInfo"
  21. | "playerBarMiddleButtons"
  22. | "playerBarRightControls"
  23. | "popupContainer";
  24. // YT only
  25. export type YTObserverName =
  26. // | "ytMasthead" // the title bar
  27. | "ytGuide"; // the left sidebar menu
  28. /** Options that are applied to every SelectorObserver instance */
  29. const defaultObserverOptions: SelectorObserverOptions = {
  30. disableOnNoListeners: false,
  31. enableOnAddListener: false,
  32. defaultDebounce: 100,
  33. defaultDebounceEdge: "rising",
  34. };
  35. /** Global SelectorObserver instances usable throughout the script for improved performance */
  36. export const globservers = {} as Record<ObserverName, SelectorObserver>;
  37. /** Call after DOM load to initialize all SelectorObserver instances */
  38. export function initObservers() {
  39. try {
  40. //#region both sites
  41. //#region body
  42. // -> the entire <body> element - use sparingly due to performance impacts!
  43. globservers.body = new SelectorObserver(document.body, {
  44. ...defaultObserverOptions,
  45. defaultDebounce: 150,
  46. subtree: false,
  47. });
  48. globservers.body.enable();
  49. switch(getDomain()) {
  50. case "ytm": {
  51. //#region YTM
  52. //#region navBar
  53. // -> the navigation / title bar at the top of the page
  54. const navBarSelector = "ytmusic-nav-bar";
  55. globservers.navBar = new SelectorObserver(navBarSelector, {
  56. ...defaultObserverOptions,
  57. subtree: false,
  58. });
  59. globservers.body.addListener(navBarSelector, {
  60. listener: () => globservers.navBar.enable(),
  61. });
  62. //#region mainPanel
  63. // -> the main content panel - includes things like the video element
  64. const mainPanelSelector = "ytmusic-player-page #main-panel";
  65. globservers.mainPanel = new SelectorObserver(mainPanelSelector, {
  66. ...defaultObserverOptions,
  67. subtree: true,
  68. });
  69. globservers.body.addListener(mainPanelSelector, {
  70. listener: () => globservers.mainPanel.enable(),
  71. });
  72. //#region sideBar
  73. // -> the sidebar on the left side of the page
  74. const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
  75. globservers.sideBar = new SelectorObserver(sidebarSelector, {
  76. ...defaultObserverOptions,
  77. subtree: true,
  78. });
  79. globservers.body.addListener(sidebarSelector, {
  80. listener: () => globservers.sideBar.enable(),
  81. });
  82. //#region sideBarMini
  83. // -> the minimized sidebar on the left side of the page
  84. const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
  85. globservers.sideBarMini = new SelectorObserver(sideBarMiniSelector, {
  86. ...defaultObserverOptions,
  87. subtree: true,
  88. });
  89. globservers.body.addListener(sideBarMiniSelector, {
  90. listener: () => globservers.sideBarMini.enable(),
  91. });
  92. //#region sidePanel
  93. // -> the side panel on the right side of the /watch page
  94. const sidePanelSelector = "#side-panel";
  95. globservers.sidePanel = new SelectorObserver(sidePanelSelector, {
  96. ...defaultObserverOptions,
  97. subtree: true,
  98. });
  99. globservers.body.addListener(sidePanelSelector, {
  100. listener: () => globservers.sidePanel.enable(),
  101. });
  102. //#region playerBar
  103. // -> media controls bar at the bottom of the page
  104. const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
  105. globservers.playerBar = new SelectorObserver(playerBarSelector, {
  106. ...defaultObserverOptions,
  107. defaultDebounce: 200,
  108. });
  109. globservers.body.addListener(playerBarSelector, {
  110. listener: () => {
  111. globservers.playerBar.enable();
  112. },
  113. });
  114. //#region playerBarInfo
  115. // -> song title, artist, album, etc. inside the player bar
  116. const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
  117. globservers.playerBarInfo = new SelectorObserver(playerBarInfoSelector, {
  118. ...defaultObserverOptions,
  119. attributes: true,
  120. attributeFilter: ["title"],
  121. });
  122. globservers.playerBarInfo.addListener(playerBarInfoSelector, {
  123. listener: () => globservers.playerBarInfo.enable(),
  124. });
  125. //#region playerBarMiddleButtons
  126. // -> the buttons inside the player bar (like, dislike, lyrics, etc.)
  127. const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
  128. globservers.playerBarMiddleButtons = new SelectorObserver(playerBarMiddleButtonsSelector, {
  129. ...defaultObserverOptions,
  130. subtree: true,
  131. });
  132. globservers.playerBar.addListener(playerBarMiddleButtonsSelector, {
  133. listener: () => globservers.playerBarMiddleButtons.enable(),
  134. });
  135. //#region playerBarRightControls
  136. // -> the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
  137. const playerBarRightControls = "#right-controls";
  138. globservers.playerBarRightControls = new SelectorObserver(playerBarRightControls, {
  139. ...defaultObserverOptions,
  140. subtree: true,
  141. });
  142. globservers.playerBar.addListener(playerBarRightControls, {
  143. listener: () => globservers.playerBarRightControls.enable(),
  144. });
  145. //#region popupContainer
  146. // -> the container for popups (e.g. the queue popup)
  147. const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
  148. globservers.popupContainer = new SelectorObserver(popupContainerSelector, {
  149. ...defaultObserverOptions,
  150. subtree: true,
  151. });
  152. globservers.body.addListener(popupContainerSelector, {
  153. listener: () => globservers.popupContainer.enable(),
  154. });
  155. break;
  156. }
  157. case "yt": {
  158. //#region YT
  159. //#region ytGuide
  160. // -> the left sidebar menu
  161. const ytGuideSelector = "#content tp-yt-app-drawer#guide #guide-inner-content";
  162. globservers.ytGuide = new SelectorObserver(ytGuideSelector, {
  163. ...defaultObserverOptions,
  164. subtree: true,
  165. });
  166. globservers.body.addListener(ytGuideSelector, {
  167. listener: () => globservers.ytGuide.enable(),
  168. });
  169. // //#region ytMasthead
  170. // -> the masthead (title bar) at the top of the page
  171. // const mastheadSelector = "#content ytd-masthead#masthead";
  172. // globservers.ytMasthead = new SelectorObserver(mastheadSelector, {
  173. // ...defaultObserverOptions,
  174. // subtree: true,
  175. // });
  176. // globservers.body.addListener(mastheadSelector, {
  177. // listener: () => globservers.ytMasthead.enable(),
  178. // });
  179. }
  180. }
  181. //#region finalize
  182. emitInterface("bytm:observersReady");
  183. }
  184. catch(err) {
  185. error("Failed to initialize observers:", err);
  186. }
  187. }
  188. /**
  189. * Interface function for adding listeners to the {@linkcode globservers}
  190. * @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!
  191. * @param options Options for the listener
  192. * @template TElem The type of the element that the listener will be attached to. If set to `0`, the type HTMLElement will be used.
  193. * @template TDomain This restricts which observers are available with the current domain
  194. */
  195. export function addSelectorListener<
  196. TElem extends HTMLElement | 0 = HTMLElement,
  197. TDomain extends Domain = "ytm"
  198. >(
  199. observerName: ObserverNameByDomain<TDomain>,
  200. selector: string,
  201. options: SelectorListenerOptions<
  202. TElem extends 0
  203. ? HTMLElement
  204. : TElem
  205. >
  206. ){
  207. globservers[observerName].addListener(selector, options);
  208. }