observers.ts 7.9 KB

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