Browse Source

feat: begin working on cfg menu in yt page

Sv443 1 year ago
parent
commit
43a4a9ee11
5 changed files with 273 additions and 122 deletions
  1. 56 0
      src/components/genericButton.ts
  2. 1 0
      src/components/index.ts
  3. 26 2
      src/features/layout.ts
  4. 8 2
      src/index.ts
  5. 182 118
      src/observers.ts

+ 56 - 0
src/components/genericButton.ts

@@ -0,0 +1,56 @@
+import { getResourceUrl, onInteraction } from "../utils";
+import type { ResourceKey } from "../types";
+
+type CreateGenericBtnOptions = {
+  /** Resource key for the button icon */
+  resourceName: ResourceKey | "_";
+  /** Tooltip and aria-label of the button */
+  title: string;
+}
+& (
+  {
+    /** URL to navigate to when the button is clicked */
+    href: string;
+    onClick?: undefined;
+  } | {
+    href?: undefined;
+    /** Callback function to execute when the button is clicked */
+    onClick: (event: MouseEvent | KeyboardEvent) => void;
+  }
+);
+
+/**
+ * Creates a generic button element.  
+ * If `href` is provided, the button will be an anchor element.  
+ * If `onClick` is provided, the button will be a div element.
+ */
+export async function createGenericBtn({
+  resourceName,
+  title,
+  href,
+  onClick,
+}: CreateGenericBtnOptions) {
+  let btnElem: HTMLElement;
+  if(href) {
+    btnElem = document.createElement("a");
+    (btnElem as HTMLAnchorElement).href = href;
+    btnElem.role = "button";
+    (btnElem as HTMLAnchorElement).target = "_blank";
+    (btnElem as HTMLAnchorElement).rel = "noopener noreferrer";
+  }
+  else {
+    btnElem = document.createElement("div");
+    onClick && onInteraction(btnElem, onClick);
+  }
+
+  btnElem.classList.add("bytm-generic-btn");
+  btnElem.ariaLabel = btnElem.title = title;
+
+  const imgElem = document.createElement("img");
+  imgElem.classList.add("bytm-generic-btn-img");
+  imgElem.src = await getResourceUrl(resourceName);
+
+  btnElem.appendChild(imgElem);
+
+  return btnElem;
+}

+ 1 - 0
src/components/index.ts

@@ -1,3 +1,4 @@
 export * from "./BytmDialog";
+export * from "./genericButton";
 export * from "./hotkeyInput";
 export * from "./toggleInput";

+ 26 - 2
src/features/layout.ts

@@ -5,6 +5,7 @@ import { addSelectorListener } from "../observers";
 import { error, getResourceUrl, log, warn, t, onInteraction, getBestThumbnailUrl, getDomain } from "../utils";
 import { scriptInfo } from "../constants";
 import { openCfgMenu } from "../menu/menu_old";
+import { createGenericBtn } from "../components";
 import "./layout.css";
 
 //#MARKER BYTM-Config buttons
@@ -100,8 +101,8 @@ function exchangeLogo() {
   });
 }
 
-/** Called whenever the avatar popover menu exists to add a BYTM-Configuration button to the user menu popover */
-export async function addConfigMenuOption(container: HTMLElement) {
+/** Called whenever the avatar popover menu exists on YTM to add a BYTM config menu button to the user menu popover */
+export async function addConfigMenuOptionYTM(container: HTMLElement) {
   const cfgOptElem = document.createElement("div");
   cfgOptElem.className = "bytm-cfg-menu-option";
   
@@ -143,6 +144,29 @@ export async function addConfigMenuOption(container: HTMLElement) {
   log("Added BYTM-Configuration button to menu popover");
 }
 
+/** Called whenever the titlebar (masthead) exists on YT to add a BYTM config menu button */
+export async function addConfigMenuOptionYT(container: HTMLElement) {
+  const btnElem = await createGenericBtn({
+    resourceName: "img-logo",
+    title: t("open_menu_tooltip", scriptInfo.name),
+    onClick(e) {
+      if((!e.shiftKey && !e.ctrlKey) || logoExchanged)
+        openCfgMenu();
+      if(!logoExchanged && (e.shiftKey || e.ctrlKey))
+        exchangeLogo();
+    },
+  });
+
+  const firstChild = container.firstElementChild;
+
+  if(firstChild)
+    container.insertBefore(btnElem, firstChild);
+  else {
+    const notifEl = container.querySelector("ytd-notification-topbar-button-renderer");
+    notifEl && insertAfter(notifEl, btnElem);
+  }
+}
+
 //#MARKER remove upgrade tab
 
 /** Removes the "Upgrade" / YT Music Premium tab from the sidebar */

+ 8 - 2
src/index.ts

@@ -27,7 +27,7 @@ import {
   // lyrics
   addMediaCtrlLyricsBtn,
   // menu
-  addConfigMenuOption,
+  addConfigMenuOptionYT, addConfigMenuOptionYTM,
   // other
   initVersionCheck, initLyricsCache,
 } from "./features/index";
@@ -146,7 +146,7 @@ async function onDomLoad() {
       }
 
       addSelectorListener("body", "tp-yt-iron-dropdown #contentWrapper ytd-multi-page-menu-renderer #container.menu-container", {
-        listener: addConfigMenuOption,
+        listener: addConfigMenuOptionYTM,
       });
 
       if(features.arrowKeySupport)
@@ -187,6 +187,12 @@ async function onDomLoad() {
       ftInit.push(initVolumeFeatures());
     }
 
+    if(domain === "yt") {
+      addSelectorListener<0, "yt">("ytGuide", "#sections ytd-guide-section-renderer:nth-child(5) #items ytd-guide-entry-renderer:nth-child(1)", {
+        listener: (el) => el.parentElement && addConfigMenuOptionYT(el.parentElement),
+      });
+    }
+
     if(["ytm", "yt"].includes(domain)) {
       if(features.switchBetweenSites)
         ftInit.push(initSiteSwitch(domain));

+ 182 - 118
src/observers.ts

@@ -1,9 +1,20 @@
 import { SelectorListenerOptions, SelectorObserver, SelectorObserverOptions } from "@sv443-network/userutils";
 import { emitInterface } from "./interface";
 import { error, getDomain } from "./utils";
+import type { Domain } from "./types";
 
-export type ObserverName =
-  | "body"
+/** Names of all available Observer instances across all sites */
+export type ObserverName = SharedObserverName | YTMObserverName | YTObserverName;
+
+/** Observer names available to each site */
+export type ObserverNameByDomain<TDomain extends Domain> = SharedObserverName | (TDomain extends "ytm" ? YTMObserverName : YTObserverName);
+
+// both YTM and YT
+export type SharedObserverName =
+  | "body";
+
+// YTM only
+export type YTMObserverName =
   | "navBar"
   | "mainPanel"
   | "sideBar"
@@ -15,9 +26,15 @@ export type ObserverName =
   | "playerBarRightControls"
   | "popupContainer";
 
+// YT only
+export type YTObserverName =
+  // | "ytMasthead"
+  | "ytGuide";
+
 /** Options that are applied to every SelectorObserver instance */
 const defaultObserverOptions: SelectorObserverOptions = {
   defaultDebounce: 100,
+  defaultDebounceEdge: "rising",
 };
 
 /** Global SelectorObserver instances usable throughout the script for improved performance */
@@ -26,129 +43,162 @@ export const globservers = {} as Record<ObserverName, SelectorObserver>;
 /** Call after DOM load to initialize all SelectorObserver instances */
 export function initObservers() {
   try {
+    //#MARKER both sites
+
     // #SECTION body = the entire <body> element - use sparingly due to performance impacts!
     globservers.body = new SelectorObserver(document.body, {
       ...defaultObserverOptions,
+      defaultDebounce: 150,
       subtree: false,
     });
 
     globservers.body.enable();
 
-    if(getDomain() !== "ytm")
-      return;
-
-    //#SECTION navBar = the navigation / title bar at the top of the page
-    const navBarSelector = "ytmusic-nav-bar";
-    globservers.navBar = new SelectorObserver(navBarSelector, {
-      ...defaultObserverOptions,
-      subtree: false,
-    });
-
-    globservers.body.addListener(navBarSelector, {
-      listener: () => globservers.navBar.enable(),
-    });
-
-    // #SECTION mainPanel = the main content panel - includes things like the video element
-    const mainPanelSelector = "ytmusic-player-page #main-panel";
-    globservers.mainPanel = new SelectorObserver(mainPanelSelector, {
-      ...defaultObserverOptions,
-      subtree: true,
-    });
-
-    globservers.body.addListener(mainPanelSelector, {
-      listener: () => globservers.mainPanel.enable(),
-    });
-
-    // #SECTION sideBar = the sidebar on the left side of the page
-    const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
-    globservers.sideBar = new SelectorObserver(sidebarSelector, {
-      ...defaultObserverOptions,
-      subtree: true,
-    });
-
-    globservers.body.addListener(sidebarSelector, {
-      listener: () => globservers.sideBar.enable(),
-    });
-
-    // #SECTION sideBarMini = the minimized sidebar on the left side of the page
-    const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
-    globservers.sideBarMini = new SelectorObserver(sideBarMiniSelector, {
-      ...defaultObserverOptions,
-      subtree: true,
-    });
-
-    globservers.body.addListener(sideBarMiniSelector, {
-      listener: () => globservers.sideBarMini.enable(),
-    });
-
-    // #SECTION sidePanel = the side panel on the right side of the /watch page
-    const sidePanelSelector = "#side-panel";
-    globservers.sidePanel = new SelectorObserver(sidePanelSelector, {
-      ...defaultObserverOptions,
-      subtree: true,
-    });
-
-    globservers.body.addListener(sidePanelSelector, {
-      listener: () => globservers.sidePanel.enable(),
-    });
-
-    // #SECTION playerBar = media controls bar at the bottom of the page
-    const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
-    globservers.playerBar = new SelectorObserver(playerBarSelector, {
-      ...defaultObserverOptions,
-      defaultDebounce: 200,
-    });
-
-    globservers.body.addListener(playerBarSelector, {
-      listener: () => {
-        globservers.playerBar.enable();
-      },
-    });
-
-    // #SECTION playerBarInfo = song title, artist, album, etc. inside the player bar
-    const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
-    globservers.playerBarInfo = new SelectorObserver(playerBarInfoSelector, {
-      ...defaultObserverOptions,
-      attributes: true,
-      attributeFilter: ["title"],
-    });
-
-    globservers.playerBarInfo.addListener(playerBarInfoSelector, {
-      listener: () => globservers.playerBarInfo.enable(),
-    });
-
-    // #SECTION playerBarMiddleButtons = the buttons inside the player bar (like, dislike, lyrics, etc.)
-    const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
-    globservers.playerBarMiddleButtons = new SelectorObserver(playerBarMiddleButtonsSelector, {
-      ...defaultObserverOptions,
-      subtree: true,
-    });
-
-    globservers.playerBar.addListener(playerBarMiddleButtonsSelector, {
-      listener: () => globservers.playerBarMiddleButtons.enable(),
-    });
-
-    // #SECTION playerBarRightControls = the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
-    const playerBarRightControls = ".right-controls .middle-controls-buttons";
-    globservers.playerBarRightControls = new SelectorObserver(playerBarRightControls, {
-      ...defaultObserverOptions,
-      subtree: true,
-    });
-
-    globservers.playerBar.addListener(playerBarRightControls, {
-      listener: () => globservers.playerBarRightControls.enable(),
-    });
-
-    // #SECTION popupContainer = the container for popups (e.g. the queue popup)
-    const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
-    globservers.popupContainer = new SelectorObserver(popupContainerSelector, {
-      ...defaultObserverOptions,
-      subtree: true,
-    });
-
-    globservers.body.addListener(popupContainerSelector, {
-      listener: () => globservers.popupContainer.enable(),
-    });
+    switch(getDomain()) {
+    case "ytm": {
+      //#MARKER YTM
+
+      //#SECTION navBar = the navigation / title bar at the top of the page
+      const navBarSelector = "ytmusic-nav-bar";
+      globservers.navBar = new SelectorObserver(navBarSelector, {
+        ...defaultObserverOptions,
+        subtree: false,
+      });
+
+      globservers.body.addListener(navBarSelector, {
+        listener: () => globservers.navBar.enable(),
+      });
+
+      // #SECTION mainPanel = the main content panel - includes things like the video element
+      const mainPanelSelector = "ytmusic-player-page #main-panel";
+      globservers.mainPanel = new SelectorObserver(mainPanelSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.body.addListener(mainPanelSelector, {
+        listener: () => globservers.mainPanel.enable(),
+      });
+
+      // #SECTION sideBar = the sidebar on the left side of the page
+      const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
+      globservers.sideBar = new SelectorObserver(sidebarSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.body.addListener(sidebarSelector, {
+        listener: () => globservers.sideBar.enable(),
+      });
+
+      // #SECTION sideBarMini = the minimized sidebar on the left side of the page
+      const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
+      globservers.sideBarMini = new SelectorObserver(sideBarMiniSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.body.addListener(sideBarMiniSelector, {
+        listener: () => globservers.sideBarMini.enable(),
+      });
+
+      // #SECTION sidePanel = the side panel on the right side of the /watch page
+      const sidePanelSelector = "#side-panel";
+      globservers.sidePanel = new SelectorObserver(sidePanelSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.body.addListener(sidePanelSelector, {
+        listener: () => globservers.sidePanel.enable(),
+      });
+
+      // #SECTION playerBar = media controls bar at the bottom of the page
+      const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
+      globservers.playerBar = new SelectorObserver(playerBarSelector, {
+        ...defaultObserverOptions,
+        defaultDebounce: 200,
+      });
+
+      globservers.body.addListener(playerBarSelector, {
+        listener: () => {
+          globservers.playerBar.enable();
+        },
+      });
+
+      // #SECTION playerBarInfo = song title, artist, album, etc. inside the player bar
+      const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
+      globservers.playerBarInfo = new SelectorObserver(playerBarInfoSelector, {
+        ...defaultObserverOptions,
+        attributes: true,
+        attributeFilter: ["title"],
+      });
+
+      globservers.playerBarInfo.addListener(playerBarInfoSelector, {
+        listener: () => globservers.playerBarInfo.enable(),
+      });
+
+      // #SECTION playerBarMiddleButtons = the buttons inside the player bar (like, dislike, lyrics, etc.)
+      const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
+      globservers.playerBarMiddleButtons = new SelectorObserver(playerBarMiddleButtonsSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.playerBar.addListener(playerBarMiddleButtonsSelector, {
+        listener: () => globservers.playerBarMiddleButtons.enable(),
+      });
+
+      // #SECTION playerBarRightControls = the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
+      const playerBarRightControls = ".right-controls .middle-controls-buttons";
+      globservers.playerBarRightControls = new SelectorObserver(playerBarRightControls, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.playerBar.addListener(playerBarRightControls, {
+        listener: () => globservers.playerBarRightControls.enable(),
+      });
+
+      // #SECTION popupContainer = the container for popups (e.g. the queue popup)
+      const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
+      globservers.popupContainer = new SelectorObserver(popupContainerSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.body.addListener(popupContainerSelector, {
+        listener: () => globservers.popupContainer.enable(),
+      });
+
+      break;
+    }
+    case "yt": {
+      //#MARKER YT
+
+      // #SECTION ytGuide = the left sidebar menu
+      const ytGuideSelector = "#content tp-yt-app-drawer#guide #guide-inner-content";
+      globservers.ytGuide = new SelectorObserver(ytGuideSelector, {
+        ...defaultObserverOptions,
+        subtree: true,
+      });
+
+      globservers.body.addListener(ytGuideSelector, {
+        listener: () => globservers.ytGuide.enable(),
+      });
+
+      // // #SECTION ytMasthead = the masthead 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(),
+      // });
+    }
+    }
 
     //#SECTION finalize
 
@@ -162,7 +212,21 @@ export function initObservers() {
 /**
  * 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>(observerName: ObserverName, selector: string, options: SelectorListenerOptions<TElem>) {
+export function addSelectorListener<
+  TElem extends HTMLElement | 0,
+  TDomain extends Domain = "ytm"
+>(
+  observerName: ObserverNameByDomain<TDomain>,
+  selector: string,
+  options: SelectorListenerOptions<
+    TElem extends 0
+      ? HTMLElement
+      : TElem
+  >
+){
   globservers[observerName].addListener(selector, options);
 }