diff --git a/src/assets/img/featureMenuGrid.svg b/src/assets/img/featureMenuGrid.svg
new file mode 100644
index 00000000..fe666b78
--- /dev/null
+++ b/src/assets/img/featureMenuGrid.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/src/assets/img/maximize.svg b/src/assets/img/maximize.svg
index 1832546c..616f002c 100644
--- a/src/assets/img/maximize.svg
+++ b/src/assets/img/maximize.svg
@@ -3,50 +3,23 @@
stroke="currentColor"
class="w-6 h-6"
fill="none"
- height="100%"
+ height="24"
version="1.1"
- viewBox="0 0 36 36"
- width="100%"
+ viewBox="0 0 23.999999 24.000001"
+ width="24"
id="svg7037"
- sodipodi:docname="maximize.svg"
- inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
-
-
-
diff --git a/src/features/featureMenu/index.ts b/src/features/featureMenu/index.ts
new file mode 100644
index 00000000..f6a73f47
--- /dev/null
+++ b/src/features/featureMenu/index.ts
@@ -0,0 +1,133 @@
+import type { YouTubePlayerDiv } from "@/src/types";
+import eventManager from "@/src/utils/EventManager";
+import { isWatchPage, isShortsPage, createTooltip } from "@/src/utils/utilities";
+
+function createFeatureMenu() {
+ // Create the feature menu div
+ const featureMenu = document.createElement("div");
+ featureMenu.id = "yte-feature-menu";
+ featureMenu.style.display = "none";
+ featureMenu.classList.add("ytp-popup");
+ featureMenu.classList.add("ytp-settings-menu");
+
+ // Create the feature menu panel
+ const featureMenuPanel = document.createElement("div");
+ featureMenuPanel.classList.add("ytp-panel");
+ featureMenuPanel.style.display = "contents";
+
+ // Append the panel to the menu
+ featureMenu.appendChild(featureMenuPanel);
+
+ // Create the panel menu
+ const featureMenuPanelMenu = document.createElement("div");
+ featureMenuPanelMenu.classList.add("ytp-panel-menu");
+ featureMenuPanelMenu.id = "yte-panel-menu";
+ featureMenuPanel.appendChild(featureMenuPanelMenu);
+
+ return featureMenu;
+}
+
+function createFeatureMenuButton() {
+ // Check if the feature menu already exists
+ const featureMenuExists = document.querySelector("#yte-feature-menu") as HTMLDivElement | null;
+ const featureMenu = featureMenuExists ? (document.querySelector("#yte-feature-menu") as HTMLDivElement) : createFeatureMenu();
+
+ // Create the feature menu button
+ const featureMenuButton = document.createElement("button");
+ featureMenuButton.classList.add("ytp-button");
+ featureMenuButton.id = "yte-feature-menu-button";
+ featureMenuButton.dataset.title = "Feature menu";
+ featureMenuButton.style.display = "none";
+ // Create the SVG icon for the button
+ const featureButtonSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ const featureButtonSVGPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ featureButtonSVG.setAttribute("viewBox", "0 0 36 36");
+ featureButtonSVG.setAttribute("height", "48px");
+ featureButtonSVG.setAttribute("width", "48px");
+ featureButtonSVG.setAttribute("fill", "white");
+ featureButtonSVGPath.setAttribute(
+ "d",
+ "M 9.1273596,13.56368 H 13.56368 V 9.1273596 H 9.1273596 Z M 15.78184,26.872641 h 4.43632 V 22.43632 h -4.43632 z m -6.6544804,0 H 13.56368 V 22.43632 H 9.1273596 Z m 0,-6.654481 H 13.56368 V 15.78184 H 9.1273596 Z m 6.6544804,0 h 4.43632 V 15.78184 H 15.78184 Z M 22.43632,9.1273596 V 13.56368 h 4.436321 V 9.1273596 Z M 15.78184,13.56368 h 4.43632 V 9.1273596 h -4.43632 z m 6.65448,6.65448 h 4.436321 V 15.78184 H 22.43632 Z m 0,6.654481 h 4.436321 V 22.43632 H 22.43632 Z"
+ );
+ featureButtonSVGPath.setAttribute("fill", "white");
+ featureButtonSVG.appendChild(featureButtonSVGPath);
+ featureMenuButton.appendChild(featureButtonSVG);
+
+ // Get references to various elements and check their existence
+ const settingsButton = document.querySelector("button.ytp-settings-button") as HTMLButtonElement | null;
+ if (!settingsButton) return;
+ const playerContainer = isWatchPage() ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) : isShortsPage() ? null : null;
+ if (!playerContainer) return;
+ const bottomControls = document.querySelector("div.ytp-chrome-bottom") as HTMLDivElement | null;
+ if (!bottomControls) return;
+
+ // Create a tooltip for the feature menu button
+ const { listener: featureMenuButtonMouseOverListener, remove: removeFeatureMenuTooltip } = createTooltip({
+ featureName: "featureMenu",
+ id: "yte-feature-menu-tooltip",
+ element: featureMenuButton
+ });
+
+ // Event listeners for showing and hiding the feature menu
+ eventManager.addEventListener(
+ featureMenuButton,
+ "click",
+ () => {
+ const featureMenuVisible = featureMenu.style.display === "block";
+ if (featureMenuVisible) {
+ bottomControls.style.opacity = "";
+ featureMenu.style.display = "none";
+ featureMenuButtonMouseOverListener();
+ } else {
+ removeFeatureMenuTooltip();
+ bottomControls.style.opacity = "1";
+ featureMenu.style.display = "block";
+ }
+ },
+ "featureMenu"
+ );
+
+ eventManager.addEventListener(
+ featureMenuButton,
+ "mouseover",
+ () => {
+ const featureMenuVisible = featureMenu.style.display === "block";
+ if (featureMenuVisible) return;
+ featureMenuButtonMouseOverListener();
+ },
+ "featureMenu"
+ );
+
+ eventManager.addEventListener(
+ featureMenuButton,
+ "mouseleave",
+ () => {
+ const featureMenuVisible = featureMenu.style.display === "block";
+ if (featureMenuVisible) return;
+ removeFeatureMenuTooltip();
+ },
+ "featureMenu"
+ );
+
+ // Event listener to hide the menu when clicking outside
+ document.addEventListener("click", (event) => {
+ if (!featureMenuButton) return;
+ if (event.target === featureMenuButton) return;
+ if (event.target === featureMenu) return;
+ if (!featureMenu.contains(event.target as Node)) {
+ featureMenu.style.display = "none";
+ bottomControls.style.opacity = "";
+ }
+ });
+
+ // Insert the feature menu button and feature menu itself
+ settingsButton.insertAdjacentElement("beforebegin", featureMenuButton);
+ playerContainer.insertAdjacentElement("afterbegin", featureMenu);
+}
+
+// Function to enable the feature menu
+export function enableFeatureMenu() {
+ const featureMenuButtonExists = document.querySelector("#yte-feature-menu-button") as HTMLButtonElement | null;
+ if (featureMenuButtonExists) return;
+ createFeatureMenuButton();
+}
diff --git a/src/features/featureMenu/utils.ts b/src/features/featureMenu/utils.ts
new file mode 100644
index 00000000..0b829a50
--- /dev/null
+++ b/src/features/featureMenu/utils.ts
@@ -0,0 +1,175 @@
+import type { FeatureMenuItemIconId, FeatureMenuItemId, FeatureMenuItemLabelId, WithId } from "@/src/types";
+import eventManager, { type FeatureName } from "@/src/utils/EventManager";
+import { waitForAllElements } from "@/src/utils/utilities";
+/**
+ * Adds a feature item to the feature menu.
+ * @param icon - The SVG icon for the feature item.
+ * @param label - The label for the feature item.
+ * @param listener - The callback function when the item is clicked.
+ * @param featureName - The name of the feature.
+ * @param isToggle - (Optional) Indicates if the item is a toggle.
+ */
+export async function addFeatureItemToMenu({
+ icon,
+ label,
+ listener,
+ featureName,
+ isToggle = false
+}: {
+ icon: SVGElement;
+ label: string;
+ listener: () => void;
+ featureName: FeatureName;
+ isToggle?: boolean;
+}) {
+ // Wait for the feature menu to exist
+ await waitForAllElements(["#yte-feature-menu"]);
+
+ // Get the feature menu
+ const featureMenu = document.querySelector("#yte-feature-menu") as HTMLDivElement | null;
+ if (!featureMenu) return;
+
+ // Check if the feature item already exists in the menu
+ const featureExistsInMenu = featureMenu.querySelector(`#yte-feature-${featureName}`) as HTMLDivElement | null;
+ if (featureExistsInMenu) {
+ const menuItem = getFeatureMenuItem(featureName);
+ if (!menuItem) return;
+ eventManager.removeEventListener(menuItem, "click", featureName);
+ eventManager.addEventListener(
+ menuItem,
+ "click",
+ () => {
+ listener();
+ if (isToggle) menuItem.ariaChecked = menuItem.ariaChecked ? (!JSON.parse(menuItem.ariaChecked)).toString() : "false";
+ },
+ featureName
+ );
+ return;
+ }
+
+ // Get the feature menu panel
+ const featureMenuPanel = document.querySelector("#yte-panel-menu") as HTMLDivElement | null;
+ if (!featureMenuPanel) return;
+
+ // Get the IDs for the feature item
+ const { featureMenuItemIconId, featureMenuItemId, featureMenuItemLabelId } = getFeatureIds(featureName);
+
+ // Create a menu item element
+ const menuItem = document.createElement("div");
+ menuItem.classList.add("ytp-menuitem");
+ menuItem.id = featureMenuItemId;
+
+ // Create the menu item icon element
+ const menuItemIcon = document.createElement("div");
+ menuItemIcon.id = featureMenuItemIconId;
+ menuItemIcon.classList.add("ytp-menuitem-icon");
+ menuItemIcon.appendChild(icon);
+ menuItem.appendChild(menuItemIcon);
+
+ // Create the menu item label element
+ const menuItemLabel = document.createElement("div");
+ menuItemLabel.classList.add("ytp-menuitem-label");
+ menuItemLabel.textContent = label;
+ menuItemLabel.id = featureMenuItemLabelId;
+ eventManager.addEventListener(
+ menuItem,
+ "click",
+ () => {
+ listener();
+ if (isToggle) menuItem.ariaChecked = menuItem.ariaChecked ? (!JSON.parse(menuItem.ariaChecked)).toString() : "false";
+ },
+ featureName
+ );
+ menuItem.appendChild(menuItemLabel);
+
+ // If it's a toggle item, create the toggle elements
+ if (isToggle) {
+ const menuItemContent = document.createElement("div");
+ menuItemContent.classList.add("ytp-menuitem-content");
+ const menuItemToggle = document.createElement("div");
+ menuItemToggle.classList.add("ytp-menuitem-toggle-checkbox");
+ menuItemContent.appendChild(menuItemToggle);
+ menuItem.appendChild(menuItemContent);
+ menuItem.ariaChecked = "false";
+ } else {
+ const menuItemContent = document.createElement("div");
+ menuItemContent.classList.add("ytp-menuitem-content");
+ menuItem.appendChild(menuItemContent);
+ }
+
+ // Add the item to the feature menu panel
+ featureMenuPanel.appendChild(menuItem);
+
+ // Adjust the height and width of the feature menu
+ featureMenu.style.height = `${40 * featureMenuPanel.childElementCount + 16}px`;
+ featureMenu.style.width = "fit-content";
+ // Show the feature menu button since an item has been added
+ const featureMenuButton = document.querySelector("#yte-feature-menu-button") as HTMLButtonElement | null;
+ if (featureMenuButton) {
+ featureMenuButton.style.display = "initial";
+ }
+}
+/**
+ * Removes a feature item from the feature menu.
+ * @param featureName - The name of the feature to remove.
+ */
+export async function removeFeatureItemFromMenu(featureName: FeatureName) {
+ // Get the unique ID for the feature item
+ const { featureMenuItemId } = getFeatureIds(featureName);
+ // Find the feature menu
+ const featureMenu = document.querySelector("#yte-feature-menu") as HTMLDivElement | null;
+ if (!featureMenu) return;
+ // Find the feature menu panel
+ const featureMenuPanel = featureMenu.querySelector("#yte-panel-menu") as HTMLDivElement | null;
+ if (!featureMenuPanel) return;
+
+ // Find the specific feature menu item
+ const featureMenuItem = featureMenuPanel.querySelector(`#${featureMenuItemId}`) as HTMLDivElement | null;
+ if (!featureMenuItem) return;
+
+ // Remove the feature menu item
+ featureMenuItem.remove();
+
+ // Check if there are any items left in the menu
+ if (featureMenuPanel.childElementCount === 0) {
+ // If no items are left, hide the menu
+ featureMenu.style.display = "none";
+
+ // Find the feature menu button
+ const featureMenuButton = document.querySelector("#yte-feature-menu-button") as HTMLButtonElement | null;
+ if (!featureMenuButton) return;
+
+ // Hide the feature menu button since the menu is empty
+ featureMenuButton.style.display = "none";
+ }
+
+ // Adjust the height and width of the feature menu panel
+ featureMenu.style.height = `${40 * featureMenuPanel.childElementCount + 16}px`;
+}
+
+export function getFeatureIds(featureName: FeatureName): {
+ featureMenuItemIconId: FeatureMenuItemIconId;
+ featureMenuItemId: FeatureMenuItemId;
+ featureMenuItemLabelId: FeatureMenuItemLabelId;
+} {
+ const featureMenuItemIconId: FeatureMenuItemIconId = `yte-${featureName}-icon`;
+ const featureMenuItemId: FeatureMenuItemId = `yte-feature-${featureName}`;
+ const featureMenuItemLabelId: FeatureMenuItemLabelId = `yte-${featureName}-label`;
+ return {
+ featureMenuItemIconId,
+ featureMenuItemId,
+ featureMenuItemLabelId
+ };
+}
+export function getFeatureMenuItemIcon(featureName: FeatureName): HTMLDivElement | null {
+ const selector: WithId = `#yte-${featureName}-icon`;
+ return document.querySelector(selector);
+}
+export function getFeatureMenuItemLabel(featureName: FeatureName): HTMLDivElement | null {
+ const selector: WithId = `#yte-${featureName}-label`;
+ return document.querySelector(selector);
+}
+export function getFeatureMenuItem(featureName: FeatureName): HTMLDivElement | null {
+ const selector: WithId = `#yte-feature-${featureName}`;
+ return document.querySelector(selector);
+}
diff --git a/src/features/loopButton/index.ts b/src/features/loopButton/index.ts
index 850d0090..8441b71d 100644
--- a/src/features/loopButton/index.ts
+++ b/src/features/loopButton/index.ts
@@ -1,5 +1,7 @@
import { waitForSpecificMessage } from "@/src/utils/utilities";
-import { makeLoopOffButton } from "./utils";
+import { loopButtonClickListener, makeLoopIcon } from "./utils";
+import { addFeatureItemToMenu, removeFeatureItemFromMenu } from "../featureMenu/utils";
+import eventManager from "@/src/utils/EventManager";
export async function addLoopButton() {
// Wait for the "options" message from the content script
@@ -12,17 +14,22 @@ export async function addLoopButton() {
const { enable_loop_button } = options;
// If the loop button option is disabled, return
if (!enable_loop_button) return;
- const loopButtonExists = document.querySelector("button#yte-loop-button") as HTMLButtonElement | null;
- if (loopButtonExists) return;
// Get the volume control element
const volumeControl = document.querySelector("div.ytp-chrome-controls > div.ytp-left-controls > span.ytp-volume-area") as HTMLSpanElement | null;
// If volume control element is not available, return
if (!volumeControl) return;
- const loopButton = makeLoopOffButton();
- volumeControl.before(loopButton);
+ const videoElement = document.querySelector("video.html5-main-video") as HTMLVideoElement | null;
+ if (!videoElement) return;
+ const loopSVG = makeLoopIcon();
+ addFeatureItemToMenu({
+ icon: loopSVG,
+ label: `Loop`,
+ featureName: "loopButton",
+ listener: loopButtonClickListener,
+ isToggle: true
+ });
}
export function removeLoopButton() {
- const loopButton = document.querySelector("button#yte-loop-button") as HTMLButtonElement | null;
- if (!loopButton) return;
- loopButton.remove();
+ removeFeatureItemFromMenu("loopButton");
+ eventManager.removeEventListeners("loopButton");
}
diff --git a/src/features/loopButton/utils.ts b/src/features/loopButton/utils.ts
index 905b1696..35df45b8 100644
--- a/src/features/loopButton/utils.ts
+++ b/src/features/loopButton/utils.ts
@@ -1,136 +1,26 @@
-import eventManager from "@/src/utils/EventManager";
-import { createTooltip } from "@/src/utils/utilities";
+import { getFeatureMenuItem } from "../featureMenu/utils";
-async function loopButtonClickListener() {
+export async function loopButtonClickListener() {
const videoElement = document.querySelector("video.html5-main-video") as HTMLVideoElement | null;
if (!videoElement) return;
- const loopButton = document.querySelector("button#yte-loop-button") as HTMLButtonElement | null;
- if (!loopButton) return;
+ const loopMenuItem = getFeatureMenuItem("loopButton");
+ if (!loopMenuItem) return;
const loop = videoElement.hasAttribute("loop");
if (loop) {
- loopButton.dataset.title = "Loop Off";
videoElement.removeAttribute("loop");
} else {
videoElement.setAttribute("loop", "");
- loopButton.dataset.title = "Loop On";
}
- const loopButtonSVG = loop ? makeLoopOffSVG() : makeLoopOnSVG();
- loopButton.removeChild(loopButton.firstChild as Node);
- loopButton.appendChild(loopButtonSVG);
}
-export function makeLoopOnButton() {
- const loopOnButton = document.createElement("button");
- loopOnButton.classList.add("ytp-button");
- loopOnButton.id = "yte-loop-button";
- loopOnButton.dataset.title = "Loop On";
- loopOnButton.style.padding = "0px";
- loopOnButton.style.display = "flex";
- loopOnButton.style.alignItems = "center";
- loopOnButton.style.justifyContent = "center";
- const loopOnButtonSVG = makeLoopOnSVG();
- loopOnButton.appendChild(loopOnButtonSVG);
- const { listener: loopOnButtonMouseOverListener, update } = createTooltip({
- element: loopOnButton,
- id: "yte-loop-button-tooltip",
- featureName: "loopButton"
- });
- eventManager.addEventListener(
- loopOnButton,
- "click",
- () => {
- loopButtonClickListener();
- update();
- },
- "loopButton"
- );
- eventManager.addEventListener(loopOnButton, "mouseover", loopOnButtonMouseOverListener, "loopButton");
- return loopOnButton;
-}
-export function makeLoopOffButton() {
- const loopOffButton = document.createElement("button");
- loopOffButton.classList.add("ytp-button");
- loopOffButton.id = "yte-loop-button";
- loopOffButton.dataset.title = "Loop Off";
- loopOffButton.style.padding = "0px";
- loopOffButton.style.display = "flex";
- loopOffButton.style.alignItems = "center";
- loopOffButton.style.justifyContent = "center";
- const loopOffButtonSVG = makeLoopOffSVG();
- loopOffButton.appendChild(loopOffButtonSVG);
- const { listener: loopOffButtonListener, update } = createTooltip({
- element: loopOffButton,
- id: "yte-loop-button-tooltip",
- featureName: "loopButton"
- });
- eventManager.addEventListener(
- loopOffButton,
- "click",
- () => {
- loopButtonClickListener();
- update();
- },
- "loopButton"
- );
- eventManager.addEventListener(loopOffButton, "mouseover", loopOffButtonListener, "loopButton");
- return loopOffButton;
-}
-function makeLoopOnSVG(): SVGElement {
- const loopOnSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- loopOnSVG.setAttributeNS(null, "stroke", "currentColor");
- loopOnSVG.setAttributeNS(null, "stroke-width", "1.5");
- loopOnSVG.setAttributeNS(null, "fill", "currentColor");
- loopOnSVG.setAttributeNS(null, "height", "36");
- loopOnSVG.setAttributeNS(null, "width", "36");
- loopOnSVG.setAttributeNS(null, "viewBox", "0 0 36 36");
- const loopOnGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
- loopOnGroup.setAttributeNS(null, "transform", "matrix(0.0943489,0,0,-0.09705882,-1.9972187,36.735291)");
- const loopOnTopArrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- loopOnTopArrowPath.setAttributeNS(
- null,
- "d",
- "m 120.59273,172.42419 v 20.60606 20.60606 l -1e-5,20.60608 v 20.60606 h 40.55254 40.55253 40.55255 40.55253 v 30.9091 l 50.69068,-41.21213 -50.69068,-51.51516 v 30.9091 h -32.94893 -32.94893 -32.94895 -32.94893 v -12.8788 -12.87879 -12.87879 -12.87879 h -7.6036 -7.6036 -7.6036 z"
- );
- loopOnTopArrowPath.setAttributeNS(null, "transform", "matrix(1.0454545,0,0,0.99999979,-14.814644,20.606103)");
- const loopOnBottomArrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- loopOnBottomArrowPath.setAttributeNS(
- null,
- "d",
- "m 313.21727,172.4242 1e-5,-82.42427 H 151.00712 V 59.09087 l -50.69065,41.21209 50.69065,51.51516 v -30.90909 h 131.79575 v 51.51517 z"
- );
- loopOnBottomArrowPath.setAttributeNS(null, "transform", "matrix(1.0454545,0,0,0.99999979,-14.814644,20.606103)");
- loopOnGroup.appendChild(loopOnTopArrowPath);
- loopOnGroup.appendChild(loopOnBottomArrowPath);
- loopOnSVG.appendChild(loopOnGroup);
- return loopOnSVG;
-}
-function makeLoopOffSVG(): SVGElement {
- const loopOffSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- loopOffSVG.setAttributeNS(null, "stroke", "currentColor");
- loopOffSVG.setAttributeNS(null, "stroke-width", "1.5");
- loopOffSVG.setAttributeNS(null, "fill", "currentColor");
- loopOffSVG.setAttributeNS(null, "height", "36");
- loopOffSVG.setAttributeNS(null, "width", "36");
- loopOffSVG.setAttributeNS(null, "viewBox", "0 0 36 36");
- const loopOffG = document.createElementNS("http://www.w3.org/2000/svg", "g");
- loopOffG.setAttributeNS(null, "transform", "matrix(0.09863748,0,0,-0.0970588,-3.3949621,34.735285)");
- const loopOffTopArrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- loopOffTopArrowPath.setAttributeNS(
- null,
- "d",
- "M 282.80287,285.75755 V 270.303 254.84845 h -81.10508 -57.44282 l 35.48347,-30.9091 h 103.06443 v -15.45454 -15.45455 l 25.34533,25.75758 25.34534,25.75758 -25.34534,20.60607 z M 151.00712,211.73026 v -39.30607 h -15.2072 -15.2072 v 65.93941 z"
- );
- const loopOffBottomArrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- loopOffBottomArrowPath.setAttributeNS(
- null,
- "d",
- "m 282.80287,172.42419 v -25.75758 -12.51658 l 30.4144,-26.48201 v 23.54404 41.21213 h -15.2072 z M 151.00712,151.81812 125.66179,126.06054 100.31645,100.30296 125.66179,79.696895 151.00712,59.090829 v 15.45455 15.454549 h 81.10508 58.45648 l -35.48347,30.909102 h -38.18022 -65.89787 v 15.45455 z"
- );
- const loopOffLinePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- loopOffLinePath.setAttributeNS(null, "d", "M 7,10 28,28 29.981167,25.855305 8.9811674,7.8553045 Z");
- loopOffLinePath.setAttributeNS(null, "transform", "matrix(10.138134,0,0,-10.303033,29.349514,357.87878)");
- loopOffG.appendChild(loopOffTopArrowPath);
- loopOffG.appendChild(loopOffBottomArrowPath);
- loopOffG.appendChild(loopOffLinePath);
- loopOffSVG.appendChild(loopOffG);
- return loopOffSVG;
+export function makeLoopIcon() {
+ const loopSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ loopSVG.setAttributeNS(null, "stroke-width", "0");
+ loopSVG.setAttributeNS(null, "fill", "white");
+ loopSVG.setAttributeNS(null, "height", "24");
+ loopSVG.setAttributeNS(null, "width", "24");
+ loopSVG.setAttributeNS(null, "viewBox", "0 0 24 24");
+ const loopPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ loopPath.setAttributeNS(null, "d", "M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z");
+ loopSVG.appendChild(loopPath);
+ return loopSVG;
}
diff --git a/src/features/maximizePlayerButton/index.ts b/src/features/maximizePlayerButton/index.ts
index f3f7fc3e..efabcada 100644
--- a/src/features/maximizePlayerButton/index.ts
+++ b/src/features/maximizePlayerButton/index.ts
@@ -1,7 +1,8 @@
import type { YouTubePlayerDiv } from "@/src/types";
import eventManager from "@/src/utils/EventManager";
-import { createTooltip, waitForSpecificMessage } from "@/src/utils/utilities";
+import { waitForSpecificMessage } from "@/src/utils/utilities";
import { makeMaximizeSVG, updateProgressBarPositions, setupVideoPlayerTimeUpdate, maximizePlayer } from "./utils";
+import { addFeatureItemToMenu, getFeatureMenuItem, removeFeatureItemFromMenu } from "../featureMenu/utils";
// TODO: fix the "default/theatre" view button and pip button not making the player minimize to the previous state.
export async function addMaximizePlayerButton(): Promise {
// Wait for the "options" message from the content script
@@ -14,45 +15,14 @@ export async function addMaximizePlayerButton(): Promise {
const { enable_maximize_player_button: enableMaximizePlayerButton } = options;
// If the maximize player button option is disabled, return
if (!enableMaximizePlayerButton) return;
- const maximizePlayerButtonExists = document.querySelector("button.yte-maximize-player-button") as HTMLButtonElement | null;
- // If maximize player button already exists, return
- if (maximizePlayerButtonExists) return;
- // Get the volume control element
- const sizeButton = document.querySelector(
- "div.ytp-chrome-controls > div.ytp-right-controls > button.ytp-button.ytp-size-button"
- ) as HTMLButtonElement | null;
- // If volume control element is not available, return
- if (!sizeButton) return;
- // Create the maximize player button element
- const maximizePlayerButton = document.createElement("button");
- maximizePlayerButton.classList.add("ytp-button");
- maximizePlayerButton.classList.add("yte-maximize-player-button");
- maximizePlayerButton.dataset.title = "Maximize Player";
const maximizeSVG = makeMaximizeSVG();
- maximizePlayerButton.appendChild(maximizeSVG);
// Add a click event listener to the maximize button
async function maximizePlayerButtonClickListener() {
- maximizePlayer(maximizePlayerButton);
+ maximizePlayer();
updateProgressBarPositions();
setupVideoPlayerTimeUpdate();
}
- // Append the maximize player button to before the volume control element
- sizeButton.before(maximizePlayerButton);
- const { listener: maximizePlayerButtonMouseOverListener, update } = createTooltip({
- featureName: "maximizePlayerButton",
- element: maximizePlayerButton,
- id: "yte-maximize-player-button-tooltip"
- });
- eventManager.addEventListener(
- maximizePlayerButton,
- "click",
- () => {
- maximizePlayerButtonClickListener();
- update();
- },
- "maximizePlayerButton"
- );
- eventManager.addEventListener(maximizePlayerButton, "mouseover", maximizePlayerButtonMouseOverListener, "maximizePlayerButton");
+
const pipElement: HTMLButtonElement | null = document.querySelector("button.ytp-pip-button");
const sizeElement: HTMLButtonElement | null = document.querySelector("button.ytp-size-button");
const miniPlayerElement: HTMLButtonElement | null = document.querySelector("button.ytp-miniplayer-button");
@@ -64,9 +34,19 @@ export async function addMaximizePlayerButton(): Promise {
const videoContainer = document.querySelector("#movie_player") as YouTubePlayerDiv | null;
if (!videoContainer) return;
if (videoContainer.classList.contains("maximized_video_container") && videoElement.classList.contains("maximized_video")) {
- maximizePlayer(maximizePlayerButton);
+ const maximizePlayerMenuItem = getFeatureMenuItem("maximizePlayerButton");
+ if (!maximizePlayerMenuItem) return;
+ maximizePlayer();
+ maximizePlayerMenuItem.ariaChecked = "false";
}
}
+ addFeatureItemToMenu({
+ featureName: "maximizePlayerButton",
+ icon: maximizeSVG,
+ label: "Maximize Player",
+ listener: maximizePlayerButtonClickListener,
+ isToggle: true
+ });
function ytpLeftButtonMouseEnterListener(event: MouseEvent) {
const ytTooltip = document.querySelector("#movie_player > div.ytp-tooltip") as HTMLDivElement | null;
if (!ytTooltip) return;
@@ -176,11 +156,6 @@ export async function addMaximizePlayerButton(): Promise {
});
}
export async function removeMaximizePlayerButton(): Promise {
- // Try to get the existing maximize player button element
- const maximizePlayerButton = document.querySelector("button.yte-maximize-player-button") as HTMLButtonElement | null;
- // If maximize player button is not available, return
- if (!maximizePlayerButton) return;
- // Remove the maximize player button element
- maximizePlayerButton.remove();
+ removeFeatureItemFromMenu("maximizePlayerButton");
eventManager.removeEventListeners("maximizePlayerButton");
}
diff --git a/src/features/maximizePlayerButton/utils.ts b/src/features/maximizePlayerButton/utils.ts
index 278ceacf..3c9b8af9 100644
--- a/src/features/maximizePlayerButton/utils.ts
+++ b/src/features/maximizePlayerButton/utils.ts
@@ -9,19 +9,19 @@ export function makeMaximizeSVG(): SVGElement {
maximizeSVG.setAttributeNS(null, "width", "100%");
maximizeSVG.setAttributeNS(null, "fill", "none");
maximizeSVG.setAttributeNS(null, "stroke-width", "1.5");
- maximizeSVG.setAttributeNS(null, "viewBox", "0 0 36 36");
+ maximizeSVG.setAttributeNS(null, "viewBox", "0 0 24 24");
const maximize_SVG_FirstPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
maximize_SVG_FirstPath.setAttributeNS(
null,
"d",
- "M 26.171872,26.171876 H 9.8281282 V 9.8281241 H 26.171872 Z m -16.3437437,0 V 9.8281241 H 26.171872 V 26.171876 Z"
+ "M 21.283309,21.283314 H 2.7166914 V 2.7166868 H 21.283309 Z m -18.5666175,0 V 2.7166868 H 21.283309 V 21.283314 Z"
);
maximize_SVG_FirstPath.setAttributeNS(null, "stroke-linecap", "round");
maximize_SVG_FirstPath.setAttributeNS(null, "stroke-linejoin", "round");
maximize_SVG_FirstPath.style.strokeWidth = "1.5";
maximize_SVG_FirstPath.style.strokeLinejoin = "round";
const maximize_SVG_SecondPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- maximize_SVG_SecondPath.setAttributeNS(null, "d", "m 18,14.497768 v 7.004464 M 21.502231,18 h -7.004462");
+ maximize_SVG_SecondPath.setAttributeNS(null, "d", "M 12,8.0214379 V 15.978562 M 15.978561,12 H 8.0214389");
maximize_SVG_SecondPath.setAttributeNS(null, "stroke-linecap", "round");
maximize_SVG_SecondPath.setAttributeNS(null, "stroke-linejoin", "round");
maximize_SVG_SecondPath.style.strokeWidth = "1.5";
@@ -30,34 +30,8 @@ export function makeMaximizeSVG(): SVGElement {
maximizeSVG.appendChild(maximize_SVG_SecondPath);
return maximizeSVG;
}
-export function makeMinimizeSVG(): SVGElement {
- const minimizeSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- minimizeSVG.setAttributeNS(null, "stroke", "currentColor");
- minimizeSVG.setAttributeNS(null, "height", "100%");
- minimizeSVG.setAttributeNS(null, "width", "100%");
- minimizeSVG.setAttributeNS(null, "fill", "none");
- minimizeSVG.setAttributeNS(null, "stroke-width", "1.5");
- minimizeSVG.setAttributeNS(null, "viewBox", "0 0 36 36");
- const minimize_SVG_FirstPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- minimize_SVG_FirstPath.setAttributeNS(
- null,
- "d",
- "M 26.171872,26.171876 H 9.8281282 V 9.8281241 H 26.171872 Z m -16.3437437,0 V 9.8281241 H 26.171872 V 26.171876 Z"
- );
- minimize_SVG_FirstPath.setAttributeNS(null, "stroke-linecap", "round");
- minimize_SVG_FirstPath.setAttributeNS(null, "stroke-linejoin", "round");
- minimize_SVG_FirstPath.style.strokeWidth = "1.5";
- minimize_SVG_FirstPath.style.strokeLinejoin = "round";
- const minimize_SVG_SecondPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- minimize_SVG_SecondPath.setAttributeNS(null, "d", "M 21.502231,18 H 14.497769");
- minimize_SVG_SecondPath.setAttributeNS(null, "stroke-linecap", "round");
- minimize_SVG_SecondPath.setAttributeNS(null, "stroke-linejoin", "round");
- minimize_SVG_SecondPath.style.strokeWidth = "1.5";
- minimize_SVG_SecondPath.style.strokeLinejoin = "round";
- minimizeSVG.appendChild(minimize_SVG_FirstPath);
- minimizeSVG.appendChild(minimize_SVG_SecondPath);
- return minimizeSVG;
-} // TODO: get played progress bar to be accurate when maximized from default view
+
+// TODO: get played progress bar to be accurate when maximized from default view
// TODO: Add event listener that updates scrubber position when maximize button is clicked
export function updateProgressBarPositions() {
const seekBar = document.querySelector("div.ytp-progress-bar") as HTMLDivElement | null;
@@ -84,7 +58,7 @@ export function setupVideoPlayerTimeUpdate() {
};
eventManager.addEventListener(videoElement, "timeupdate", videoPlayerTimeUpdateListener, "maximizePlayerButton");
}
-export function maximizePlayer(maximizePlayerButton: HTMLButtonElement) {
+export function maximizePlayer() {
// Get the video element
const videoElement = document.querySelector("video.video-stream.html5-main-video") as HTMLVideoElement | null;
// If video element is not available, return
@@ -143,13 +117,10 @@ export function maximizePlayer(maximizePlayerButton: HTMLButtonElement) {
// sizeElement.dataset.titleNoTooltip = dataTitleNoTooltip;
// sizeElement.title = title;
// }
-
- maximizePlayerButton.dataset.title = "Maximize Player";
+ document.body.style.overflow = "";
videoElement.classList.remove("maximized_video");
videoContainer.classList.remove("maximized_video_container");
controlsElement.classList.remove("maximized_controls");
- maximizePlayerButton.removeChild(maximizePlayerButton.firstChild as Node);
- maximizePlayerButton.appendChild(makeMaximizeSVG());
} else {
// sizeElement.click();
@@ -166,12 +137,9 @@ export function maximizePlayer(maximizePlayerButton: HTMLButtonElement) {
// sizeElement.dataset.titleNoTooltip = dataTitleNoTooltip;
// sizeElement.title = title;
// }
-
- maximizePlayerButton.dataset.title = "Minimize Player";
+ document.body.style.overflow = "hidden";
videoElement.classList.add("maximized_video");
videoContainer.classList.add("maximized_video_container");
controlsElement.classList.add("maximized_controls");
- maximizePlayerButton.removeChild(maximizePlayerButton.firstChild as Node);
- maximizePlayerButton.appendChild(makeMinimizeSVG());
}
}
diff --git a/src/features/remainingTime/index.ts b/src/features/remainingTime/index.ts
index 9c00b1c0..7e8b7de8 100644
--- a/src/features/remainingTime/index.ts
+++ b/src/features/remainingTime/index.ts
@@ -14,7 +14,7 @@ async function playerTimeUpdateListener() {
if (!playerContainer) return;
// Get the video element
- const videoElement = playerContainer.childNodes[0]?.childNodes[0] as HTMLVideoElement | null;
+ const videoElement = playerContainer.querySelector("video") as HTMLVideoElement | null;
// If video element is not available, return
if (!videoElement) return;
@@ -46,7 +46,7 @@ export async function setupRemainingTime() {
// If player element is not available, return
if (!playerContainer) return;
// Get the video element
- const videoElement = playerContainer.childNodes[0]?.childNodes[0] as HTMLVideoElement | null;
+ const videoElement = playerContainer.querySelector("video") as HTMLVideoElement | null;
// If video element is not available, return
if (!videoElement) return;
const remainingTime = await calculateRemainingTime({ videoElement, playerContainer });
diff --git a/src/features/screenshotButton/index.ts b/src/features/screenshotButton/index.ts
index d71a9651..c4219c34 100644
--- a/src/features/screenshotButton/index.ts
+++ b/src/features/screenshotButton/index.ts
@@ -1,5 +1,6 @@
import eventManager from "@/src/utils/EventManager";
-import { createTooltip, waitForSpecificMessage } from "@/src/utils/utilities";
+import { waitForSpecificMessage } from "@/src/utils/utilities";
+import { addFeatureItemToMenu, removeFeatureItemFromMenu } from "../featureMenu/utils";
async function takeScreenshot(videoElement: HTMLVideoElement) {
try {
@@ -65,44 +66,7 @@ export async function addScreenshotButton(): Promise {
const { enable_screenshot_button: enableScreenshotButton } = options;
// If the screenshot button option is disabled, return
if (!enableScreenshotButton) return;
- const screenshotButtonExists = document.querySelector("button.yte-screenshot-button") as HTMLButtonElement | null;
- // If screenshot button already exists, return
- if (screenshotButtonExists) return;
- // Get the volume control element
- const volumeControl = document.querySelector("div.ytp-chrome-controls > div.ytp-left-controls > span.ytp-volume-area") as HTMLSpanElement | null;
- // If volume control element is not available, return
- if (!volumeControl) return;
- // Create the screenshot button element
- const screenshotButton = document.createElement("button");
- screenshotButton.classList.add("ytp-button");
- screenshotButton.classList.add("yte-screenshot-button");
- screenshotButton.dataset.title = "Screenshot";
- screenshotButton.style.padding = "0px";
- screenshotButton.style.display = "flex";
- screenshotButton.style.alignItems = "center";
- screenshotButton.style.justifyContent = "center";
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.style.height = "28px";
- svg.style.width = "28px";
- svg.setAttributeNS(null, "stroke-width", "1.5");
- svg.setAttributeNS(null, "stroke", "currentColor");
- svg.setAttributeNS(null, "fill", "none");
- svg.setAttributeNS(null, "viewBox", "0 0 24 24");
- const firstPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- firstPath.setAttributeNS(
- null,
- "d",
- "M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"
- );
- firstPath.setAttributeNS(null, "stroke-linecap", "round");
- firstPath.setAttributeNS(null, "stroke-linejoin", "round");
- const secondPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
- secondPath.setAttributeNS(null, "d", "M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z");
- secondPath.setAttributeNS(null, "stroke-linecap", "round");
- secondPath.setAttributeNS(null, "stroke-linejoin", "round");
- svg.appendChild(firstPath);
- svg.appendChild(secondPath);
- screenshotButton.appendChild(svg);
+
// Add a click event listener to the screenshot button
async function screenshotButtonClickListener() {
// Get the video element
@@ -116,30 +80,38 @@ export async function addScreenshotButton(): Promise {
console.error(error);
}
}
- // Append the screenshot button to before the volume control element
- volumeControl.before(screenshotButton);
- const { listener: screenshotButtonMouseOverListener, update } = createTooltip({
- element: screenshotButton,
+ addFeatureItemToMenu({
featureName: "screenshotButton",
- id: "yte-screenshot-tooltip"
+ icon: makeScreenshotIcon(),
+ label: "Screenshot",
+ listener: screenshotButtonClickListener
});
- eventManager.addEventListener(
- screenshotButton,
- "click",
- () => {
- screenshotButtonClickListener();
- update();
- },
- "screenshotButton"
- );
- eventManager.addEventListener(screenshotButton, "mouseover", screenshotButtonMouseOverListener, "screenshotButton");
}
export async function removeScreenshotButton(): Promise {
- // Try to get the existing screenshot button element
- const screenshotButton = document.querySelector("button.yte-screenshot-button") as HTMLButtonElement | null;
- // If screenshot button is not available, return
- if (!screenshotButton) return;
- // Remove the screenshot button element
- screenshotButton.remove();
+ removeFeatureItemFromMenu("screenshotButton");
eventManager.removeEventListeners("screenshotButton");
}
+function makeScreenshotIcon() {
+ const screenshotSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ screenshotSVG.style.height = "24px";
+ screenshotSVG.style.width = "24px";
+ screenshotSVG.setAttributeNS(null, "stroke-width", "1.5");
+ screenshotSVG.setAttributeNS(null, "stroke", "currentColor");
+ screenshotSVG.setAttributeNS(null, "fill", "none");
+ screenshotSVG.setAttributeNS(null, "viewBox", "0 0 24 24");
+ const firstPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ firstPath.setAttributeNS(
+ null,
+ "d",
+ "M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"
+ );
+ firstPath.setAttributeNS(null, "stroke-linecap", "round");
+ firstPath.setAttributeNS(null, "stroke-linejoin", "round");
+ const secondPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ secondPath.setAttributeNS(null, "d", "M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z");
+ secondPath.setAttributeNS(null, "stroke-linecap", "round");
+ secondPath.setAttributeNS(null, "stroke-linejoin", "round");
+ screenshotSVG.appendChild(firstPath);
+ screenshotSVG.appendChild(secondPath);
+ return screenshotSVG;
+}
diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx
index 91738881..3c82a7dd 100644
--- a/src/pages/content/index.tsx
+++ b/src/pages/content/index.tsx
@@ -1,17 +1,19 @@
-import type { ExtensionSendOnlyMessageMappings, Messages, YouTubePlayerDiv } from "@/src/types";
-import { browserColorLog, formatError } from "@/utils/utilities";
-import eventManager from "@/utils/EventManager";
+import { enableFeatureMenu } from "@/src/features/featureMenu";
+import { addLoopButton, removeLoopButton } from "@/src/features/loopButton";
import { addMaximizePlayerButton, removeMaximizePlayerButton } from "@/src/features/maximizePlayerButton";
import { maximizePlayer } from "@/src/features/maximizePlayerButton/utils";
import setPlayerQuality from "@/src/features/playerQuality";
import setPlayerSpeed from "@/src/features/playerSpeed";
+import { removeRemainingTimeDisplay, setupRemainingTime } from "@/src/features/remainingTime";
import enableRememberVolume from "@/src/features/rememberVolume";
import { addScreenshotButton, removeScreenshotButton } from "@/src/features/screenshotButton";
import adjustVolumeOnScrollWheel from "@/src/features/scrollWheelVolumeControl";
-import { setupVideoHistory, promptUserToResumeVideo } from "@/src/features/videoHistory";
+import { promptUserToResumeVideo, setupVideoHistory } from "@/src/features/videoHistory";
import volumeBoost from "@/src/features/volumeBoost";
-import { removeRemainingTimeDisplay, setupRemainingTime } from "@/src/features/remainingTime";
-import { addLoopButton, removeLoopButton } from "@/src/features/loopButton";
+import eventManager from "@/utils/EventManager";
+import { browserColorLog, formatError } from "@/utils/utilities";
+
+import type { ExtensionSendOnlyMessageMappings, Messages, YouTubePlayerDiv } from "@/src/types";
// TODO: Add always show progressbar feature
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -64,11 +66,12 @@ document.documentElement.appendChild(element);
window.onload = function () {
enableRememberVolume();
+ enableFeatureMenu();
const enableFeatures = () => {
- eventManager.removeAllEventListeners();
+ eventManager.removeAllEventListeners(["featureMenu"]);
addLoopButton();
- addScreenshotButton();
addMaximizePlayerButton();
+ addScreenshotButton();
enableRememberVolume();
setPlayerQuality();
setPlayerSpeed();
@@ -162,7 +165,7 @@ window.onload = function () {
const videoContainer = document.querySelector("#movie_player") as YouTubePlayerDiv | null;
if (!videoContainer) return;
if (videoContainer.classList.contains("maximized_video_container") && videoElement.classList.contains("maximized_video")) {
- maximizePlayer(maximizePlayerButton);
+ maximizePlayer();
}
}
break;
diff --git a/src/types.ts b/src/types.ts
index 86590254..e15b3be5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,6 @@
import z from "zod";
import type { YouTubePlayer } from "node_modules/@types/youtube-player/dist/types";
+import type { FeatureName } from "./utils/EventManager";
/* eslint-disable no-mixed-spaces-and-tabs */
export type Writeable = { -readonly [P in keyof T]: T[P] };
@@ -148,3 +149,7 @@ export type ConfigurationToZodSchema = z.ZodObject<{
export type Prettify = {
[K in keyof T]: T[K];
};
+export type FeatureMenuItemIconId = `yte-${FeatureName}-icon`;
+export type FeatureMenuItemId = `yte-feature-${FeatureName}`;
+export type FeatureMenuItemLabelId = `yte-${FeatureName}-label`;
+export type WithId = `#${S}`;
diff --git a/src/utils/EventManager.ts b/src/utils/EventManager.ts
index 0d7b0310..a64d13b0 100644
--- a/src/utils/EventManager.ts
+++ b/src/utils/EventManager.ts
@@ -8,7 +8,8 @@ export type FeatureName =
| "playerSpeed"
| "playerQuality"
| "loopButton"
- | "rememberVolume";
+ | "rememberVolume"
+ | "featureMenu";
type EventCallback = (event: HTMLElementEventMap[K]) => void;
export interface EventListenerInfo {
@@ -37,7 +38,7 @@ export type EventManager = {
removeEventListeners: (featureName: FeatureName) => void;
- removeAllEventListeners: () => void;
+ removeAllEventListeners: (exclude?: FeatureName[]) => void;
};
export const eventManager: EventManager = {
@@ -92,15 +93,15 @@ export const eventManager: EventManager = {
},
// Removes all event listeners
- removeAllEventListeners: function () {
- // Remove all event listeners from all targets
- this.listeners.forEach((targetListeners) => {
+ removeAllEventListeners: function (exclude) {
+ // Remove all event listeners from all targets excluding the given feature names
+ this.listeners.forEach((targetListeners, featureName) => {
+ if (exclude && exclude.includes(featureName)) return;
targetListeners.forEach(({ target, eventName, callback }) => {
target.removeEventListener(eventName, callback);
});
+ this.listeners.delete(featureName);
});
- // Remove all maps of target listeners from the map
- this.listeners.clear();
}
};
export default eventManager;
diff --git a/src/utils/utilities.ts b/src/utils/utilities.ts
index ef3a100d..eb8a6616 100644
--- a/src/utils/utilities.ts
+++ b/src/utils/utilities.ts
@@ -337,31 +337,12 @@ export function formatError(error: unknown) {
*/
export function waitForAllElements(selectors: Selector[]): Promise {
return new Promise((resolve) => {
- setTimeout(() => {
- browserColorLog("Waiting for target nodes", "FgMagenta");
- const elementsMap = new Map();
- const { length: selectorsCount } = selectors;
- let resolvedCount = 0;
-
- const observer = new MutationObserver(() => {
- selectors.forEach((selector) => {
- const element = document.querySelector(selector);
- elementsMap.set(selector, element);
- if (!element) {
- return;
- }
-
- resolvedCount++;
- if (resolvedCount === selectorsCount) {
- observer.disconnect();
- const resolvedElements = selectors.map((selector) => (elementsMap.get(selector) ? selector : undefined)).filter(Boolean);
- resolve(resolvedElements);
- }
- });
- });
-
- observer.observe(document, { childList: true, subtree: true });
+ browserColorLog("Waiting for target nodes", "FgMagenta");
+ const elementsMap = new Map();
+ const { length: selectorsCount } = selectors;
+ let resolvedCount = 0;
+ const observer = new MutationObserver(() => {
selectors.forEach((selector) => {
const element = document.querySelector(selector);
elementsMap.set(selector, element);
@@ -376,7 +357,24 @@ export function waitForAllElements(selectors: Selector[]): Promise {
resolve(resolvedElements);
}
});
- }, 2_500);
+ });
+
+ observer.observe(document, { childList: true, subtree: true });
+
+ selectors.forEach((selector) => {
+ const element = document.querySelector(selector);
+ elementsMap.set(selector, element);
+ if (!element) {
+ return;
+ }
+
+ resolvedCount++;
+ if (resolvedCount === selectorsCount) {
+ observer.disconnect();
+ const resolvedElements = selectors.map((selector) => (elementsMap.get(selector) ? selector : undefined)).filter(Boolean);
+ resolve(resolvedElements);
+ }
+ });
});
}
export function settingsAreDefault(defaultSettings: Partial, currentSettings: Partial): boolean {
@@ -437,28 +435,38 @@ export function createTooltip({ element, text, id, featureName }: { text?: strin
remove: () => void;
update: () => void;
} {
+ function makeTooltip() {
+ // Create tooltip element
+ const tooltip = document.createElement("div");
+ const rect = element.getBoundingClientRect();
+ tooltip.classList.add("yte-button-tooltip");
+ tooltip.classList.add("ytp-tooltip");
+ tooltip.classList.add("ytp-rounded-tooltip");
+ tooltip.classList.add("ytp-bottom");
+ tooltip.id = id;
+ tooltip.style.left = `${rect.left + rect.width / 2}px`;
+ tooltip.style.top = `${rect.top - 2}px`;
+ tooltip.style.zIndex = "99999";
+ const {
+ dataset: { title }
+ } = element;
+ tooltip.textContent = text ?? title ?? "";
+ function mouseLeaveListener() {
+ tooltip.remove();
+ eventManager.removeEventListener(element, "mouseleave", featureName);
+ }
+ eventManager.addEventListener(element, "mouseleave", mouseLeaveListener, featureName);
+ return tooltip;
+ }
return {
listener: () => {
- // Create tooltip element
- const tooltip = document.createElement("div");
- const rect = element.getBoundingClientRect();
- tooltip.classList.add("yte-button-tooltip");
- tooltip.classList.add("ytp-tooltip");
- tooltip.classList.add("ytp-rounded-tooltip");
- tooltip.classList.add("ytp-bottom");
- tooltip.id = id;
- tooltip.style.left = `${rect.left + rect.width / 2}px`;
- tooltip.style.top = `${rect.top - 2}px`;
- tooltip.style.zIndex = "99999";
- const {
- dataset: { title }
- } = element;
- tooltip.textContent = text ?? title ?? "";
- function mouseLeaveListener() {
+ const tooltipExists = document.getElementById(id) !== null;
+ if (tooltipExists) {
+ const tooltip = document.getElementById(id);
+ if (!tooltip) return;
tooltip.remove();
- eventManager.removeEventListener(element, "mouseleave", featureName);
}
- eventManager.addEventListener(element, "mouseleave", mouseLeaveListener, featureName);
+ const tooltip = makeTooltip();
document.body.appendChild(tooltip);
},
remove: () => {