Skip to content

Commit

Permalink
Merge pull request #68 from VampireChicken12/dev
Browse files Browse the repository at this point in the history
Add "Feature menu" button
  • Loading branch information
VampireChicken12 authored Nov 2, 2023
2 parents 30f68c7 + 2b730db commit 7b1e25d
Show file tree
Hide file tree
Showing 14 changed files with 486 additions and 369 deletions.
7 changes: 7 additions & 0 deletions src/assets/img/featureMenuGrid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 7 additions & 34 deletions src/assets/img/maximize.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions src/features/featureMenu/index.ts
Original file line number Diff line number Diff line change
@@ -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();
}
175 changes: 175 additions & 0 deletions src/features/featureMenu/utils.ts
Original file line number Diff line number Diff line change
@@ -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<FeatureMenuItemIconId> = `#yte-${featureName}-icon`;
return document.querySelector(selector);
}
export function getFeatureMenuItemLabel(featureName: FeatureName): HTMLDivElement | null {
const selector: WithId<FeatureMenuItemLabelId> = `#yte-${featureName}-label`;
return document.querySelector(selector);
}
export function getFeatureMenuItem(featureName: FeatureName): HTMLDivElement | null {
const selector: WithId<FeatureMenuItemId> = `#yte-feature-${featureName}`;
return document.querySelector(selector);
}
Loading

0 comments on commit 7b1e25d

Please sign in to comment.