generated from JohnBra/vite-web-extension
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #68 from VampireChicken12/dev
Add "Feature menu" button
- Loading branch information
Showing
14 changed files
with
486 additions
and
369 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.