siteEvents.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { createNanoEvents } from "nanoevents";
  2. import { error, info } from "./utils";
  3. import { FeatureConfig } from "./types";
  4. import { emitInterface } from "./interface";
  5. export interface SiteEventsMap {
  6. // misc:
  7. /** Emitted whenever the feature config is changed - initialization is not counted */
  8. configChanged: (config: FeatureConfig) => void;
  9. // TODO: implement
  10. /** Emitted whenever a config option is changed - contains the old and the new values */
  11. configOptionChanged: <TKey extends keyof FeatureConfig>(key: TKey, oldValue: FeatureConfig[TKey], newValue: FeatureConfig[TKey]) => void;
  12. /** Emitted whenever the config menu should be rebuilt, like when a config was imported */
  13. rebuildCfgMenu: (config: FeatureConfig) => void;
  14. /** Emitted whenever the config menu is closed */
  15. cfgMenuClosed: () => void;
  16. /** Emitted when the welcome menu is closed */
  17. welcomeMenuClosed: () => void;
  18. /** Emitted whenever the user interacts with a hotkey input, used so other keyboard input event listeners don't get called while mid-input */
  19. hotkeyInputActive: (active: boolean) => void;
  20. // DOM:
  21. /** Emitted whenever child nodes are added to or removed from the song queue */
  22. queueChanged: (queueElement: HTMLElement) => void;
  23. /** Emitted whenever child nodes are added to or removed from the autoplay queue underneath the song queue */
  24. autoplayQueueChanged: (queueElement: HTMLElement) => void;
  25. }
  26. /** Array of all site events */
  27. export const allSiteEvents = [
  28. "configChanged",
  29. "configOptionChanged",
  30. "rebuildCfgMenu",
  31. "cfgMenuClosed",
  32. "welcomeMenuClosed",
  33. "hotkeyInputActive",
  34. "queueChanged",
  35. "autoplayQueueChanged",
  36. ] as const;
  37. /** EventEmitter instance that is used to detect changes to the site */
  38. export const siteEvents = createNanoEvents<SiteEventsMap>();
  39. let observers: MutationObserver[] = [];
  40. /** Disconnects and deletes all observers. Run `initSiteEvents()` again to create new ones. */
  41. export function removeAllObservers() {
  42. observers.forEach((observer, i) => {
  43. observer.disconnect();
  44. delete observers[i];
  45. });
  46. observers = [];
  47. }
  48. /** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
  49. export async function initSiteEvents() {
  50. try {
  51. //#SECTION queue
  52. // the queue container always exists so it doesn't need an extra init function
  53. const queueObs = new MutationObserver(([ { addedNodes, removedNodes, target } ]) => {
  54. if(addedNodes.length > 0 || removedNodes.length > 0) {
  55. info(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  56. emitSiteEvent("queueChanged", target as HTMLElement);
  57. }
  58. });
  59. // only observe added or removed elements
  60. queueObs.observe(document.querySelector("#side-panel #contents.ytmusic-player-queue")!, {
  61. childList: true,
  62. });
  63. const autoplayObs = new MutationObserver(([ { addedNodes, removedNodes, target } ]) => {
  64. if(addedNodes.length > 0 || removedNodes.length > 0) {
  65. info(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  66. emitSiteEvent("autoplayQueueChanged", target as HTMLElement);
  67. }
  68. });
  69. autoplayObs.observe(document.querySelector("#side-panel ytmusic-player-queue #automix-contents")!, {
  70. childList: true,
  71. });
  72. info("Successfully initialized SiteEvents observers");
  73. observers = observers.concat([
  74. queueObs,
  75. autoplayObs,
  76. ]);
  77. }
  78. catch(err) {
  79. error("Couldn't initialize SiteEvents observers due to an error:\n", err);
  80. }
  81. }
  82. /** Emits a site event with the given key and arguments */
  83. export function emitSiteEvent<TKey extends keyof SiteEventsMap>(key: TKey, ...args: Parameters<SiteEventsMap[TKey]>) {
  84. siteEvents.emit(key, ...args);
  85. emitInterface(`bytm:siteEvent:${key}`, args as unknown as undefined);
  86. }