Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Feature menu" button #68

Merged
merged 2 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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