From 2fdd45c137e05cd80ae82c4ecf7c902bf4b451fd Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Sat, 20 Jan 2024 14:43:30 -0500 Subject: [PATCH] feat: button placement #247 --- public/locales/en-US.json | 20 ++- public/locales/en-US.json.d.ts | 11 +- src/assets/img/loopOff.svg | 8 + src/assets/img/loopOn.svg | 7 + src/assets/img/maximize.svg | 28 +--- src/assets/img/minimize.svg | 60 +------- src/assets/img/volumeBoostOff.svg | 8 + src/assets/img/volumeBoostOn.svg | 8 + src/features/buttonPlacement/index.ts | 40 +++-- src/features/buttonPlacement/utils.ts | 56 +++++-- src/features/featureMenu/index.ts | 6 +- src/features/featureMenu/utils.ts | 3 +- src/features/index.ts | 14 +- src/features/loopButton/index.ts | 29 ++-- src/features/loopButton/utils.ts | 7 +- src/features/maximizePlayerButton/index.ts | 62 +++++--- src/features/maximizePlayerButton/utils.ts | 2 +- src/features/openTranscriptButton/index.ts | 6 +- src/features/openTranscriptButton/utils.ts | 13 +- src/features/screenshotButton/index.ts | 43 +++--- src/features/scrollWheelSpeedControl/index.ts | 2 +- src/features/scrollWheelSpeedControl/utils.ts | 3 +- src/features/videoHistory/index.ts | 5 +- src/features/volumeBoost/index.ts | 22 ++- src/icons.ts | 138 ++++++++++-------- src/pages/content/index.tsx | 44 ++++-- src/pages/inject/index.tsx | 75 ++++++---- src/types/index.ts | 13 +- src/utils/constants.ts | 14 +- src/utils/utilities.ts | 56 +++++-- 30 files changed, 479 insertions(+), 324 deletions(-) create mode 100644 src/assets/img/loopOff.svg create mode 100644 src/assets/img/loopOn.svg create mode 100644 src/assets/img/volumeBoostOff.svg create mode 100644 src/assets/img/volumeBoostOn.svg diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 40bc62f3..5ef01e7d 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -12,13 +12,21 @@ "label": "Feature menu" }, "loopButton": { - "label": "Loop" + "label": "Loop", + "toggle": { + "off": "Loop off", + "on": "Loop on" + } }, "maximizePlayerButton": { - "label": "Maximize" + "label": "Maximize", + "toggle": { + "off": "Maximize off", + "on": "Maximize on" + } }, "openTranscriptButton": { - "label": "Open Transcript" + "label": "Open transcript" }, "screenshotButton": { "copiedToClipboard": "Screenshot copied to clipboard", @@ -31,7 +39,11 @@ } }, "volumeBoostButton": { - "label": "Volume Boost" + "label": "Volume Boost", + "toggle": { + "off": "Volume boost off", + "on": "Volume boost on" + } } } }, diff --git a/public/locales/en-US.json.d.ts b/public/locales/en-US.json.d.ts index 19708710..34f8dd91 100644 --- a/public/locales/en-US.json.d.ts +++ b/public/locales/en-US.json.d.ts @@ -9,12 +9,15 @@ interface EnUS { content: { features: { featureMenu: { label: "Feature menu" }; - loopButton: { label: "Loop" }; - maximizePlayerButton: { label: "Maximize" }; - openTranscriptButton: { label: "Open Transcript" }; + loopButton: { label: "Loop"; toggle: { off: "Loop off"; on: "Loop on" } }; + maximizePlayerButton: { label: "Maximize"; toggle: { off: "Maximize off"; on: "Maximize on" } }; + openTranscriptButton: { label: "Open transcript" }; screenshotButton: { copiedToClipboard: "Screenshot copied to clipboard"; label: "Screenshot" }; videoHistory: { resumeButton: "Resume"; resumePrompt: { close: "Close" } }; - volumeBoostButton: { label: "Volume Boost" }; + volumeBoostButton: { + label: "Volume Boost"; + toggle: { off: "Volume boost off"; on: "Volume boost on" }; + }; }; }; options: { diff --git a/src/assets/img/loopOff.svg b/src/assets/img/loopOff.svg new file mode 100644 index 00000000..8bee9c09 --- /dev/null +++ b/src/assets/img/loopOff.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/assets/img/loopOn.svg b/src/assets/img/loopOn.svg new file mode 100644 index 00000000..7dc4cb08 --- /dev/null +++ b/src/assets/img/loopOn.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/src/assets/img/maximize.svg b/src/assets/img/maximize.svg index 616f002c..0576082c 100644 --- a/src/assets/img/maximize.svg +++ b/src/assets/img/maximize.svg @@ -1,25 +1,7 @@ - - - - + + + diff --git a/src/assets/img/minimize.svg b/src/assets/img/minimize.svg index de5373a4..102317dc 100644 --- a/src/assets/img/minimize.svg +++ b/src/assets/img/minimize.svg @@ -1,54 +1,10 @@ - - - - - - - + + + + + + diff --git a/src/assets/img/volumeBoostOff.svg b/src/assets/img/volumeBoostOff.svg new file mode 100644 index 00000000..6eaf618f --- /dev/null +++ b/src/assets/img/volumeBoostOff.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/assets/img/volumeBoostOn.svg b/src/assets/img/volumeBoostOn.svg new file mode 100644 index 00000000..612e2190 --- /dev/null +++ b/src/assets/img/volumeBoostOn.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/features/buttonPlacement/index.ts b/src/features/buttonPlacement/index.ts index acee54f3..49ae0d9f 100644 --- a/src/features/buttonPlacement/index.ts +++ b/src/features/buttonPlacement/index.ts @@ -1,10 +1,11 @@ import type { GetIconType } from "@/src/icons"; import type { ButtonPlacement, FeaturesThatHaveButtons } from "@/src/types"; -import { waitForSpecificMessage } from "@/src/utils/utilities"; +import { addFeatureItemToMenu, removeFeatureItemFromMenu } from "@/src/features/featureMenu/utils"; +import { removeTooltip, waitForSpecificMessage } from "@/src/utils/utilities"; -import { addFeatureItemToMenu, removeFeatureItemFromMenu } from "../featureMenu/utils"; -import { type ListenerType, makeFeatureButton, placeButton } from "./utils"; +import { type ListenerType, getFeatureButtonId, makeFeatureButton, placeButton } from "./utils"; +export const featuresInControls = new Set(); export async function addFeatureButton< Name extends FeaturesThatHaveButtons, @@ -20,24 +21,28 @@ export async function addFeatureButton< case "below_player": case "player_controls_left": case "player_controls_right": { + // Add the feature name to the set of features in the controls + featuresInControls.add(featureName); const button = makeFeatureButton(featureName, placement, label, icon, listener, isToggle); placeButton(button, placement); break; } } } -export async function removeFeatureButton(featureName: FeaturesThatHaveButtons) { - // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); - if (!optionsData) return; - const { - data: { options } - } = optionsData; - // Extract the necessary properties from the options object - const { - button_placements: { [featureName]: buttonPlacement } - } = options; - switch (buttonPlacement) { +export async function removeFeatureButton(featureName: Name, placement?: ButtonPlacement) { + if (placement === undefined) { + // Wait for the "options" message from the content script + const optionsData = await waitForSpecificMessage("options", "request_data", "content"); + if (!optionsData) return; + ({ + data: { + options: { + button_placements: { [featureName]: placement } + } + } + } = optionsData); + } + switch (placement) { case "feature_menu": { removeFeatureItemFromMenu(featureName); break; @@ -45,9 +50,12 @@ export async function removeFeatureButton(featureName: FeaturesThatHaveButtons) case "below_player": case "player_controls_left": case "player_controls_right": { - const button = document.querySelector(`#yte-feature-${featureName}-button`); + // Remove the feature name from the set of features in the controls + featuresInControls.delete(featureName); + const button = document.querySelector(`#${getFeatureButtonId(featureName)}`); if (!button) return; button.remove(); + removeTooltip(`yte-feature-${featureName}-tooltip`); break; } } diff --git a/src/features/buttonPlacement/utils.ts b/src/features/buttonPlacement/utils.ts index dcf9ca6a..67fc274b 100644 --- a/src/features/buttonPlacement/utils.ts +++ b/src/features/buttonPlacement/utils.ts @@ -1,11 +1,9 @@ +import { getFeatureIds, getFeatureMenuItem } from "@/src/features/featureMenu/utils"; import { type GetIconType } from "@/src/icons"; import { type ButtonPlacement, type FeaturesThatHaveButtons } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; import { createStyledElement, createTooltip } from "@/src/utils/utilities"; -import { getFeatureIds } from "../featureMenu/utils"; -// TODO: fix icon type for toggle buttons - export type ListenerType = Toggle extends true ? (checked?: boolean) => void : () => void; function buttonClickListener( @@ -16,10 +14,9 @@ function buttonClickListener buttonClickListener(button, icon, listener, isToggle), featureName); + eventManager.addEventListener( + button, + "click", + () => { + buttonClickListener(button, icon, listener, isToggle); + update(); + }, + featureName + ); eventManager.removeEventListener(button, "mouseover", featureName); eventManager.addEventListener(button, "mouseover", tooltipListener, featureName); return button; } - button.dataset.title = label; if (isToggle) { button.ariaChecked = "false"; if (typeof icon === "object" && "off" in icon && "on" in icon) { button.append(icon.off); - } else if (typeof icon === "object" && icon instanceof SVGSVGElement) { + } else if (icon instanceof SVGSVGElement) { + button.append(icon); + } + } else { + if (icon instanceof SVGSVGElement) { button.append(icon); } } eventManager.addEventListener(button, "mouseover", tooltipListener, featureName); - eventManager.addEventListener(button, "click", () => buttonClickListener(button, icon, listener, isToggle), featureName); + eventManager.addEventListener( + button, + "click", + () => { + buttonClickListener(button, icon, listener, isToggle); + update(); + }, + featureName + ); return button; } -function updateFeatureButtonIcon(button: HTMLButtonElement, icon: SVGElement) { +export function updateFeatureButtonIcon(button: HTMLButtonElement, icon: SVGElement) { if (button.firstChild) { - button.firstChild.remove(); - button.append(icon); + button.firstChild.replaceWith(icon); } } +export function updateFeatureButtonTitle(featureName: FeaturesThatHaveButtons, title: string) { + const button = document.querySelector(`#${getFeatureButtonId(featureName)}`); + if (!button) return; + button.dataset.title = title; +} export function placeButton(button: HTMLButtonElement, placement: Exclude) { switch (placement) { case "below_player": { @@ -148,4 +169,7 @@ export function checkIfFeatureButtonExists(featureName: FeaturesThatHaveButtons, export function getFeatureButtonId(featureName: FeaturesThatHaveButtons) { return `yte-feature-${featureName}-button` as const; } +export function getFeatureButton(featureName: FeaturesThatHaveButtons) { + return getFeatureMenuItem(featureName) ?? document.querySelector(`#${getFeatureButtonId(featureName)}`); +} export const buttonContainerId = "yte-button-container"; diff --git a/src/features/featureMenu/index.ts b/src/features/featureMenu/index.ts index 383c3225..66164cc2 100644 --- a/src/features/featureMenu/index.ts +++ b/src/features/featureMenu/index.ts @@ -1,9 +1,7 @@ import type { FeatureMenuOpenType } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; -import { createSVGElement, createStyledElement, createTooltip, isWatchPage, waitForAllElements } from "@/src/utils/utilities"; - -import { waitForSpecificMessage } from "../../utils/utilities"; +import { createSVGElement, createStyledElement, createTooltip, isWatchPage, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; function createFeatureMenu() { // Create the feature menu div @@ -118,7 +116,7 @@ export function setupFeatureMenuEventListeners(featureMenuOpenType: FeatureMenuO const { listener: showFeatureMenuTooltip, remove: removeFeatureMenuTooltip } = createTooltip({ element: featureMenuButton, featureName: "featureMenu", - id: "yte-feature-menu-tooltip" + id: "yte-feature-featureMenu-tooltip" }); const hideYouTubeSettings = () => { const settingsMenu = document.querySelector("div.ytp-settings-menu:not(#yte-feature-menu)"); diff --git a/src/features/featureMenu/utils.ts b/src/features/featureMenu/utils.ts index 3ae9fbae..5a1141d2 100644 --- a/src/features/featureMenu/utils.ts +++ b/src/features/featureMenu/utils.ts @@ -1,11 +1,10 @@ +import type { ListenerType } from "@/src/features/buttonPlacement/utils"; import type { BasicIcon } from "@/src/icons"; import type { FeatureMenuItemIconId, FeatureMenuItemId, FeatureMenuItemLabelId, FeaturesThatHaveButtons, WithId } from "@/src/types"; import eventManager, { type FeatureName } from "@/src/utils/EventManager"; import { waitForAllElements } from "@/src/utils/utilities"; -import type { ListenerType } from "../buttonPlacement/utils"; - export const featuresInMenu = new Set(); function featureMenuClickListener(menuItem: HTMLDivElement, listener: ListenerType, isToggle: boolean) { diff --git a/src/features/index.ts b/src/features/index.ts index 57cecad5..71b00974 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -1,10 +1,10 @@ -import type { FeaturesThatHaveButtons } from "../types"; +import type { ButtonPlacement, FeaturesThatHaveButtons } from "@/src/types"; -import { addLoopButton, removeLoopButton } from "./loopButton"; -import { addMaximizePlayerButton, removeMaximizePlayerButton } from "./maximizePlayerButton"; -import { addOpenTranscriptButton, removeOpenTranscriptButton } from "./openTranscriptButton/utils"; -import { addScreenshotButton, removeScreenshotButton } from "./screenshotButton"; -import { addVolumeBoostButton, removeVolumeBoostButton } from "./volumeBoost"; +import { addLoopButton, removeLoopButton } from "@/src/features/loopButton"; +import { addMaximizePlayerButton, removeMaximizePlayerButton } from "@/src/features/maximizePlayerButton"; +import { addOpenTranscriptButton, removeOpenTranscriptButton } from "@/src/features/openTranscriptButton/utils"; +import { addScreenshotButton, removeScreenshotButton } from "@/src/features/screenshotButton"; +import { addVolumeBoostButton, removeVolumeBoostButton } from "@/src/features/volumeBoost"; export const featureButtonFunctions = { loopButton: { @@ -31,6 +31,6 @@ export const featureButtonFunctions = { FeaturesThatHaveButtons, { add: (() => Promise) | (() => void); - remove: (() => Promise) | (() => void); + remove: ((placement: ButtonPlacement) => Promise) | ((placement: ButtonPlacement) => void); } >; diff --git a/src/features/loopButton/index.ts b/src/features/loopButton/index.ts index 194643b8..8056450f 100644 --- a/src/features/loopButton/index.ts +++ b/src/features/loopButton/index.ts @@ -1,9 +1,12 @@ -import { getIcon } from "@/src/icons"; +import type { ButtonPlacement } from "@/src/types"; + +import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; +import { getFeatureButton, getFeatureButtonId } from "@/src/features/buttonPlacement/utils"; +import { getFeatureIds } from "@/src/features/featureMenu/utils"; +import { getFeatureIcon } from "@/src/icons"; import eventManager, { type FeatureName } from "@/src/utils/EventManager"; import { waitForSpecificMessage } from "@/src/utils/utilities"; -import { addFeatureButton, removeFeatureButton } from "../buttonPlacement"; -import { getFeatureIds, getFeatureMenuItem } from "../featureMenu/utils"; import { loopButtonClickListener } from "./utils"; export async function addLoopButton() { @@ -27,17 +30,18 @@ export async function addLoopButton() { const videoElement = document.querySelector("video.html5-main-video"); if (!videoElement) return; - const loopSVG = getIcon("loopButton", loopButtonPlacement !== "feature_menu" ? "shared_position_icon" : "feature_menu"); - // TODO: fix icon positioning await addFeatureButton( "loopButton", loopButtonPlacement, - window.i18nextInstance.t("pages.content.features.loopButton.label"), - loopSVG, + loopButtonPlacement === "feature_menu" ? + window.i18nextInstance.t("pages.content.features.loopButton.label") + : window.i18nextInstance.t("pages.content.features.loopButton.toggle.off"), + getFeatureIcon("loopButton", loopButtonPlacement !== "feature_menu" ? "shared_icon_position" : "feature_menu"), loopButtonClickListener, true ); const loopChangedHandler = (mutationList: MutationRecord[]) => { + const loopSVG = getFeatureIcon("loopButton", loopButtonPlacement !== "feature_menu" ? "shared_icon_position" : "feature_menu"); for (const mutation of mutationList) { if (mutation.type === "attributes") { const { attributeName, target } = mutation; @@ -50,17 +54,16 @@ export async function addLoopButton() { const featureExistsInMenu = featureMenu && featureMenu.querySelector(`#${getFeatureIds(featureName).featureMenuItemId}`) !== null; if (featureExistsInMenu) { - const menuItem = getFeatureMenuItem(featureName); + const menuItem = getFeatureButton(featureName); if (!menuItem) return; menuItem.ariaChecked = loop ? "true" : "false"; } - const button = document.querySelector(`#yte-feature-${featureName}-button`); + const button = document.querySelector(`#${getFeatureButtonId(featureName)}`); if (!button) return; - button.firstChild?.remove(); switch (loopButtonPlacement) { case "feature_menu": { if (loopSVG instanceof SVGSVGElement) { - button.appendChild(loopSVG); + button.firstChild?.replaceWith(loopSVG); } break; } @@ -80,7 +83,7 @@ export async function addLoopButton() { const loopChangeMutationObserver = new MutationObserver(loopChangedHandler); loopChangeMutationObserver.observe(videoElement, { attributeFilter: ["loop"], attributes: true }); } -export function removeLoopButton() { - void removeFeatureButton("loopButton"); +export function removeLoopButton(placement?: ButtonPlacement) { + void removeFeatureButton("loopButton", placement); eventManager.removeEventListeners("loopButton"); } diff --git a/src/features/loopButton/utils.ts b/src/features/loopButton/utils.ts index 9656af39..bde9613a 100644 --- a/src/features/loopButton/utils.ts +++ b/src/features/loopButton/utils.ts @@ -1,4 +1,9 @@ -export function loopButtonClickListener() { +import { updateFeatureButtonTitle } from "@/src/features/buttonPlacement/utils"; + +export function loopButtonClickListener(checked?: boolean) { + if (checked !== undefined) { + updateFeatureButtonTitle("loopButton", window.i18nextInstance.t(`pages.content.features.loopButton.toggle.${checked ? "on" : "off"}`)); + } const videoElement = document.querySelector("video.html5-main-video"); if (!videoElement) return; const loop = videoElement.hasAttribute("loop"); diff --git a/src/features/maximizePlayerButton/index.ts b/src/features/maximizePlayerButton/index.ts index 6e0b5970..b120fc75 100644 --- a/src/features/maximizePlayerButton/index.ts +++ b/src/features/maximizePlayerButton/index.ts @@ -1,11 +1,11 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { ButtonPlacement, YouTubePlayerDiv } from "@/src/types"; -import { getIcon } from "@/src/icons"; +import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; +import { getFeatureButton, updateFeatureButtonIcon, updateFeatureButtonTitle } from "@/src/features/buttonPlacement/utils"; +import { getFeatureIcon } from "@/src/icons"; import eventManager from "@/src/utils/EventManager"; -import { waitForSpecificMessage } from "@/src/utils/utilities"; +import { createTooltip, waitForSpecificMessage } from "@/src/utils/utilities"; -import { addFeatureButton, removeFeatureButton } from "../buttonPlacement"; -import { getFeatureMenuItem } from "../featureMenu/utils"; import { maximizePlayer, setupVideoPlayerTimeUpdate, updateProgressBarPositions } from "./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 { @@ -13,17 +13,33 @@ export async function addMaximizePlayerButton(): Promise { const optionsData = await waitForSpecificMessage("options", "request_data", "content"); if (!optionsData) return; const { - data: { options } + data: { + options: { + button_placements: { maximizePlayerButton: maximizePlayerButtonPlacement }, + enable_maximize_player_button: enableMaximizePlayerButton + } + } } = optionsData; - // Extract the necessary properties from the options object - const { - button_placements: { maximizePlayerButton: maximizePlayerButtonPlacement }, - enable_maximize_player_button: enableMaximizePlayerButton - } = options; // If the maximize player button option is disabled, return if (!enableMaximizePlayerButton) return; // Add a click event listener to the maximize button - function maximizePlayerButtonClickListener() { + function maximizePlayerButtonClickListener(checked?: boolean) { + const button = getFeatureButton("maximizePlayerButton"); + if (!button) return; + const featureName = "maximizePlayerButton"; + const { remove } = createTooltip({ + direction: maximizePlayerButtonPlacement === "below_player" ? "down" : "up", + element: button, + featureName, + id: `yte-feature-${featureName}-tooltip` + }); + if (checked !== undefined) { + if (checked) remove(); + updateFeatureButtonTitle( + "maximizePlayerButton", + window.i18nextInstance.t(`pages.content.features.maximizePlayerButton.toggle.${checked ? "on" : "off"}`) + ); + } maximizePlayer(); updateProgressBarPositions(); setupVideoPlayerTimeUpdate(); @@ -40,17 +56,25 @@ export async function addMaximizePlayerButton(): Promise { const videoContainer = document.querySelector("#movie_player"); if (!videoContainer) return; if (videoContainer.classList.contains("maximized_video_container") && videoElement.classList.contains("maximized_video")) { - const maximizePlayerMenuItem = getFeatureMenuItem("maximizePlayerButton"); - if (!maximizePlayerMenuItem) return; + const maximizePlayerButton = getFeatureButton("maximizePlayerButton"); + if (!maximizePlayerButton) return; maximizePlayer(); - maximizePlayerMenuItem.ariaChecked = "false"; + maximizePlayerButton.ariaChecked = "false"; + const button = getFeatureButton("maximizePlayerButton"); + const icon = getFeatureIcon("maximizePlayerButton", "shared_icon_position"); + if (button && button instanceof HTMLButtonElement) { + if (typeof icon === "object" && "off" in icon && "on" in icon) updateFeatureButtonIcon(button, icon.off); + updateFeatureButtonTitle("maximizePlayerButton", window.i18nextInstance.t("pages.content.features.maximizePlayerButton.toggle.off")); + } } } await addFeatureButton( "maximizePlayerButton", maximizePlayerButtonPlacement, - window.i18nextInstance.t("pages.content.features.maximizePlayerButton.label"), - getIcon("maximizePlayerButton", maximizePlayerButtonPlacement !== "feature_menu" ? "shared_position_icon" : "feature_menu"), + maximizePlayerButtonPlacement === "feature_menu" ? + window.i18nextInstance.t("pages.content.features.maximizePlayerButton.label") + : window.i18nextInstance.t("pages.content.features.maximizePlayerButton.toggle.off"), + getFeatureIcon("maximizePlayerButton", maximizePlayerButtonPlacement !== "feature_menu" ? "shared_icon_position" : "feature_menu"), maximizePlayerButtonClickListener, true ); @@ -160,7 +184,7 @@ export async function addMaximizePlayerButton(): Promise { eventManager.addEventListener(button, "mouseenter", ytpRightButtonMouseEnterListener, "maximizePlayerButton"); }); } -export function removeMaximizePlayerButton() { - void removeFeatureButton("maximizePlayerButton"); +export function removeMaximizePlayerButton(placement?: ButtonPlacement) { + void removeFeatureButton("maximizePlayerButton", placement); eventManager.removeEventListeners("maximizePlayerButton"); } diff --git a/src/features/maximizePlayerButton/utils.ts b/src/features/maximizePlayerButton/utils.ts index 1cf5f8de..a5d66b7d 100644 --- a/src/features/maximizePlayerButton/utils.ts +++ b/src/features/maximizePlayerButton/utils.ts @@ -1,7 +1,7 @@ import type { YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; -import { createSVGElement } from "@/src/utils/utilities"; + let wasInTheatreMode = false; let setToTheatreMode = false; diff --git a/src/features/openTranscriptButton/index.ts b/src/features/openTranscriptButton/index.ts index 0a6ee7d5..9a94c61f 100644 --- a/src/features/openTranscriptButton/index.ts +++ b/src/features/openTranscriptButton/index.ts @@ -1,7 +1,7 @@ +import { removeFeatureButton } from "@/src/features/buttonPlacement"; +import { getFeatureButton } from "@/src/features/buttonPlacement/utils"; import { waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; -import { removeFeatureButton } from "../buttonPlacement"; -import { getFeatureMenuItem } from "../featureMenu/utils"; import { addOpenTranscriptButton } from "./utils"; export async function openTranscriptButton() { @@ -17,7 +17,7 @@ export async function openTranscriptButton() { if (!enableOpenTranscriptButton) return; await waitForAllElements(["ytd-video-description-transcript-section-renderer button"]); const transcriptButton = document.querySelector("ytd-video-description-transcript-section-renderer button"); - const transcriptButtonMenuItem = getFeatureMenuItem("openTranscriptButton"); + const transcriptButtonMenuItem = getFeatureButton("openTranscriptButton"); // If the transcript button is not found and the "openTranscriptButton" menu item exists, remove the transcript button menu item if (!transcriptButton && transcriptButtonMenuItem) await removeFeatureButton("openTranscriptButton"); // If the transcript button isn't found return diff --git a/src/features/openTranscriptButton/utils.ts b/src/features/openTranscriptButton/utils.ts index c2ed7dea..7df72b89 100644 --- a/src/features/openTranscriptButton/utils.ts +++ b/src/features/openTranscriptButton/utils.ts @@ -1,9 +1,10 @@ -import { getIcon } from "@/src/icons"; +import type { ButtonPlacement } from "@/src/types"; + +import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; +import { getFeatureIcon } from "@/src/icons"; import eventManager from "@/src/utils/EventManager"; import { waitForSpecificMessage } from "@/src/utils/utilities"; -import { addFeatureButton, removeFeatureButton } from "../buttonPlacement"; - export async function addOpenTranscriptButton() { // Wait for the "options" message from the content script const optionsData = await waitForSpecificMessage("options", "request_data", "content"); @@ -24,12 +25,12 @@ export async function addOpenTranscriptButton() { "openTranscriptButton", openTranscriptButtonPlacement, window.i18nextInstance.t("pages.content.features.openTranscriptButton.label"), - getIcon("openTranscriptButton", openTranscriptButtonPlacement !== "feature_menu" ? "shared_position_icon" : "feature_menu"), + getFeatureIcon("openTranscriptButton", openTranscriptButtonPlacement !== "feature_menu" ? "shared_icon_position" : "feature_menu"), transcriptButtonClickerListener, false ); } -export function removeOpenTranscriptButton() { - void removeFeatureButton("openTranscriptButton"); +export function removeOpenTranscriptButton(placement?: ButtonPlacement) { + void removeFeatureButton("openTranscriptButton", placement); eventManager.removeEventListeners("openTranscriptButton"); } diff --git a/src/features/screenshotButton/index.ts b/src/features/screenshotButton/index.ts index 53d6895a..deea4157 100644 --- a/src/features/screenshotButton/index.ts +++ b/src/features/screenshotButton/index.ts @@ -1,9 +1,10 @@ -import { getIcon } from "@/src/icons"; -import eventManager from "@/src/utils/EventManager"; -import { waitForSpecificMessage } from "@/src/utils/utilities"; +import type { ButtonPlacement } from "@/src/types"; -import { addFeatureButton, removeFeatureButton } from "../buttonPlacement"; -import { getFeatureMenuItem } from "../featureMenu/utils"; +import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; +import { getFeatureButton } from "@/src/features/buttonPlacement/utils"; +import { getFeatureIcon } from "@/src/icons"; +import eventManager from "@/src/utils/EventManager"; +import { createTooltip, waitForSpecificMessage } from "@/src/utils/utilities"; async function takeScreenshot(videoElement: HTMLVideoElement) { try { @@ -37,25 +38,21 @@ async function takeScreenshot(videoElement: HTMLVideoElement) { switch (screenshot_save_as) { case "clipboard": { - const tooltip = document.createElement("div"); - const screenshotMenuItem = getFeatureMenuItem("screenshotButton"); - if (!screenshotMenuItem) return; - const rect = screenshotMenuItem.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 = "yte-screenshot-tooltip"; - tooltip.style.left = `${rect.left + rect.width / 2}px`; - tooltip.style.top = `${rect.top - 2}px`; - tooltip.style.zIndex = "99999"; - tooltip.textContent = window.i18nextInstance.t("pages.content.features.screenshotButton.copiedToClipboard"); - document.body.appendChild(tooltip); + const screenshotButton = getFeatureButton("screenshotButton"); + if (!screenshotButton) return; + const { listener, remove } = createTooltip({ + direction: "up", + element: screenshotButton, + featureName: "screenshotButton", + id: "yte-feature-screenshotButton-tooltip", + text: window.i18nextInstance.t("pages.content.features.screenshotButton.copiedToClipboard") + }); + listener(); const clipboardImage = new ClipboardItem({ "image/png": blob }); void navigator.clipboard.write([clipboardImage]); void navigator.clipboard.writeText(dataUrl); setTimeout(() => { - tooltip.remove(); + remove(); }, 1200); break; } @@ -105,12 +102,12 @@ export async function addScreenshotButton(): Promise { "screenshotButton", screenshotButtonPlacement, window.i18nextInstance.t("pages.content.features.screenshotButton.label"), - getIcon("screenshotButton", screenshotButtonPlacement !== "feature_menu" ? "shared_position_icon" : "feature_menu"), + getFeatureIcon("screenshotButton", screenshotButtonPlacement !== "feature_menu" ? "shared_icon_position" : "feature_menu"), screenshotButtonClickListener, false ); } -export function removeScreenshotButton() { - void removeFeatureButton("screenshotButton"); +export function removeScreenshotButton(placement?: ButtonPlacement) { + void removeFeatureButton("screenshotButton", placement); eventManager.removeEventListeners("screenshotButton"); } diff --git a/src/features/scrollWheelSpeedControl/index.ts b/src/features/scrollWheelSpeedControl/index.ts index 1bbb662e..e85d29fc 100644 --- a/src/features/scrollWheelSpeedControl/index.ts +++ b/src/features/scrollWheelSpeedControl/index.ts @@ -1,9 +1,9 @@ import type { YouTubePlayerDiv } from "@/src/types"; +import { setupScrollListeners } from "@/src/features/scrollWheelVolumeControl/utils"; import OnScreenDisplayManager from "@/src/utils/OnScreenDisplayManager"; import { isShortsPage, isWatchPage, preventScroll, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; -import { setupScrollListeners } from "../scrollWheelVolumeControl/utils"; import { adjustSpeed } from "./utils"; export default async function adjustSpeedOnScrollWheel() { diff --git a/src/features/scrollWheelSpeedControl/utils.ts b/src/features/scrollWheelSpeedControl/utils.ts index b7d3ee8b..eeea217c 100644 --- a/src/features/scrollWheelSpeedControl/utils.ts +++ b/src/features/scrollWheelSpeedControl/utils.ts @@ -1,9 +1,8 @@ import type { Selector, YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; -import { browserColorLog, clamp, toDivisible } from "@/src/utils/utilities"; +import { browserColorLog, clamp, round, toDivisible } from "@/src/utils/utilities"; -import { round } from "../../utils/utilities"; /** * Adjust the speed based on the scroll direction. * diff --git a/src/features/videoHistory/index.ts b/src/features/videoHistory/index.ts index 0e63308a..42f4c5cf 100644 --- a/src/features/videoHistory/index.ts +++ b/src/features/videoHistory/index.ts @@ -1,5 +1,6 @@ import type { VideoHistoryEntry, YouTubePlayerDiv } from "@/src/types"; +import { formatTime } from "@/src/features/remainingTime/utils"; import eventManager from "@/utils/EventManager"; import { browserColorLog, @@ -11,8 +12,6 @@ import { sendContentMessage, waitForSpecificMessage } from "@/utils/utilities"; - -import { formatTime } from "../remainingTime/utils"; export async function setupVideoHistory() { // Wait for the "options" message from the content script const optionsData = await waitForSpecificMessage("options", "request_data", "content"); @@ -231,7 +230,7 @@ function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContaine const { listener: resumePromptCloseButtonMouseOverListener } = createTooltip({ element: closeButton, featureName: "videoHistory", - id: "yte-resume-prompt-close-button-tooltip", + id: "yte-feature-videoHistory-tooltip", text: window.i18nextInstance.t("pages.content.features.videoHistory.resumePrompt.close") }); eventManager.addEventListener(closeButton, "mouseover", resumePromptCloseButtonMouseOverListener, "videoHistory"); diff --git a/src/features/volumeBoost/index.ts b/src/features/volumeBoost/index.ts index 1a4737dd..66da0c45 100644 --- a/src/features/volumeBoost/index.ts +++ b/src/features/volumeBoost/index.ts @@ -1,9 +1,11 @@ -import { getIcon } from "@/src/icons"; +import type { ButtonPlacement } from "@/src/types"; + +import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; +import { updateFeatureButtonTitle } from "@/src/features/buttonPlacement/utils"; +import { getFeatureIcon } from "@/src/icons"; import eventManager from "@/src/utils/EventManager"; import { browserColorLog, formatError, waitForSpecificMessage } from "@/src/utils/utilities"; -import { addFeatureButton, removeFeatureButton } from "../buttonPlacement"; - export default async function volumeBoost() { setupVolumeBoost(); const optionsData = await waitForSpecificMessage("options", "request_data", "content"); @@ -73,11 +75,17 @@ export async function addVolumeBoostButton() { await addFeatureButton( "volumeBoostButton", volumeBoostButtonPlacement, - window.i18nextInstance.t("pages.content.features.volumeBoostButton.label"), - getIcon("volumeBoostButton", volumeBoostButtonPlacement !== "feature_menu" ? "shared_position_icon" : "feature_menu"), + volumeBoostButtonPlacement === "feature_menu" ? + window.i18nextInstance.t("pages.content.features.volumeBoostButton.label") + : window.i18nextInstance.t(`pages.content.features.volumeBoostButton.toggle.off`), + getFeatureIcon("volumeBoostButton", volumeBoostButtonPlacement !== "feature_menu" ? "shared_icon_position" : "feature_menu"), (checked) => { void (async () => { if (checked !== undefined) { + updateFeatureButtonTitle( + "volumeBoostButton", + window.i18nextInstance.t(`pages.content.features.volumeBoostButton.toggle.${checked ? "on" : "off"}`) + ); if (checked) { await enableVolumeBoost(); } else { @@ -89,7 +97,7 @@ export async function addVolumeBoostButton() { true ); } -export function removeVolumeBoostButton() { - void removeFeatureButton("volumeBoostButton"); +export function removeVolumeBoostButton(placement?: ButtonPlacement) { + void removeFeatureButton("volumeBoostButton", placement); eventManager.removeEventListeners("volumeBoostButton"); } diff --git a/src/icons.ts b/src/icons.ts index ba043231..e2708191 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -8,12 +8,12 @@ export const toggleFeatures = Object.keys({ loopButton: "", maximizePlayerButton >); export type ToggleFeatures = (typeof toggleFeatures)[number]; export type IconType = T extends ToggleFeatures ? ToggleIcon : BasicIcon; -export type GetPlacementKey = Placement extends "feature_menu" ? "feature_menu" : "shared_position_icon"; +export type GetPlacementKey = Placement extends "feature_menu" ? "feature_menu" : "shared_icon_position"; export type GetIconType = FeatureIconsType[Name][GetPlacementKey]; export type FeatureIconsType = { [Feature in FeaturesThatHaveButtons]: { feature_menu: BasicIcon; - shared_position_icon: IconType; + shared_icon_position: IconType; }; }; @@ -21,53 +21,44 @@ const loopOnSVG = createSVGElement( "svg", { fill: "white", - height: "36", + height: "24px", stroke: "white", - "stroke-width": "1.5", - viewBox: "0 0 36 36", - width: "36" + viewBox: "0 0 24 24", + width: "24px" }, - createSVGElement( - "g", - { - transform: "matrix(0.0943489,0,0,-0.09705882,-1.9972187,36.735291)" - }, - createSVGElement("path", { - 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", - transform: "matrix(1.0454545,0,0,0.99999979,-14.814644,20.606103)" - }), - createSVGElement("path", { - 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", - transform: "matrix(1.0454545,0,0,0.99999979,-14.814644,20.606103)" - }) - ) + createSVGElement("path", { + d: "m 2.4999994,12.000002 v -1.999999 -2 l -1e-6,-2.000001 v -2 h 4.0000004 3.9999992 4.000001 4 V 1.0000021 L 23.5,5.000002 l -5.000001,5 V 7.000001 h -3.25 H 12 8.7499986 5.4999992 v 1.250001 1.25 1.25 1.25 h -0.75 -0.7499999 -0.7499999 z", + "stroke-width": 0 + }), + createSVGElement("path", { + d: "m 21.499999,12.000001 1e-6,8.000001 H 5.4999982 v 2.999996 l -4.99999798,-3.999996 4.99999798,-5 v 3 H 18.499999 v -5.000001 z", + "stroke-width": 0 + }) ); const loopOffSVG = createSVGElement( "svg", { fill: "white", - height: "36", + height: "24px", stroke: "white", - "stroke-width": "1.5", - viewBox: "0 0 36 36", - width: "36" + viewBox: "0 0 24 24", + width: "24px" }, - createSVGElement( - "g", - { - transform: "matrix(0.09863748,0,0,-0.0970588,-3.3949621,34.735285)" - }, - createSVGElement("path", { - 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" - }), - createSVGElement("path", { - 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" - }), - createSVGElement("path", { - d: "M 7,10 28,28 29.981167,25.855305 8.9811674,7.8553045 Z", - transform: "matrix(10.138134,0,0,-10.303033,29.349514,357.87878)" - }) - ) + createSVGElement("path", { + d: "M 221.97407,347.57575 V 332.1212 316.66665 H 140.86899 83.426168 l 35.483472,-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 90.178318,273.54846 v -39.30607 h -15.2072 -15.2072 v 65.93941 z", + "stroke-width": 0, + transform: "matrix(0.09863748,0,0,-0.0970588,-3.3949621,34.735285)" + }), + createSVGElement("path", { + d: "m 221.97407,234.24239 v -25.75758 -12.51658 l 30.4144,-26.48201 v 23.54404 41.21213 h -15.2072 z m -131.795752,-20.60607 -25.34533,-25.75758 -25.34534,-25.75758 25.34534,-20.60607 25.34533,-20.60606 v 15.45455 15.45455 h 81.105082 58.45648 l -35.48347,30.9091 H 156.07619 90.178318 v 15.45455 z", + "stroke-width": 0, + transform: "matrix(0.09863748,0,0,-0.0970588,-3.3949621,34.735285)" + }), + createSVGElement("path", { + d: "M 39.48765,316.66665 252.38846,131.21205 272.4738,153.30892 59.57299,338.76352 Z", + "stroke-width": 0, + transform: "matrix(0.09863748,0,0,-0.0970588,-3.3949621,34.735285)" + }) ); const screenshotButtonSVG = createSVGElement( "svg", @@ -90,7 +81,7 @@ const screenshotButtonSVG = createSVGElement( "stroke-linejoin": "round" }) ); -const volumeBoostSVG = createSVGElement( +const volumeBoostOnSVG = createSVGElement( "svg", { fill: "white", @@ -100,8 +91,30 @@ const volumeBoostSVG = createSVGElement( width: "24px" }, createSVGElement("path", { - d: "M3 9v6h4l5 5V4L7 9H3zm7-.17v6.34L7.83 13H5v-2h2.83L10 8.83zM16.5 12A4.5 4.5 0 0014 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77 0-4.28-2.99-7.86-7-8.77", - stroke: "none" + d: "m 18.678443,19.914783 c -0.237137,0 -0.474274,-0.0902 -0.654617,-0.27176 -0.361886,-0.36192 -0.361886,-0.94868 0,-1.31064 1.690919,-1.69112 2.62223,-3.9406 2.62223,-6.33336 0,-2.3927605 -0.931311,-4.6409929 -2.62223,-6.3333449 -0.361886,-0.36194 -0.361886,-0.948708 0,-1.310648 0.361885,-0.36194 0.948589,-0.36194 1.310476,0 C 21.376027,6.3969701 22.5,9.1109055 22.5,11.997783 c 0,2.88688 -1.123973,5.60084 -3.165698,7.64276 -0.180304,0.18036 -0.417482,0.2718 -0.654618,0.2718 z m -3.290446,-1.74668 c -0.237138,0 -0.474274,-0.0902 -0.654617,-0.27176 -0.361886,-0.36196 -0.361886,-0.94872 0,-1.31068 2.528358,-2.52864 2.528358,-6.6434005 0,-9.1720449 -0.361886,-0.36194 -0.361886,-0.948704 0,-1.310648 0.361885,-0.36194 0.948589,-0.36194 1.310514,0 1.57481,1.575 2.441888,3.6688124 2.441888,5.8960529 0,2.22724 -0.867078,4.32108 -2.441888,5.89608 -0.180343,0.18036 -0.41748,0.27176 -0.654657,0.27176 v 0 z m -3.291647,-1.74796 c -0.237177,0 -0.474314,-0.0902 -0.654657,-0.27176 -0.361886,-0.36192 -0.361886,-0.94872 0,-1.31064 1.564971,-1.56512 1.564971,-4.11228 0,-5.6774055 -0.361886,-0.361955 -0.361886,-0.9487194 0,-1.3106594 0.361925,-0.36194 0.948629,-0.36194 1.310515,0 2.287503,2.2877849 2.287503,6.0097049 0,8.2987049 -0.180343,0.18036 -0.41748,0.27176 -0.654618,0.27176 z", + "stroke-width": 0 + }), + createSVGElement("path", { + d: "m 9.5297155,20.647343 c -0.160584,0 -0.318688,-0.063 -0.437243,-0.18036 L 4.3321986,15.706143 H 2.1175737 c -0.3408996,0 -0.6175732,-0.27668 -0.6175732,-0.61764 V 8.9120255 c 0,-0.340933 0.2766736,-0.6176434 0.6175732,-0.6176434 h 2.2146249 l 4.7602739,-4.760824 c 0.176625,-0.176644 0.442162,-0.229764 0.67314,-0.133408 0.230978,0.096358 0.3816835,0.321176 0.3816835,0.570704 V 20.029663 c 0,0.24956 -0.1507055,0.4756 -0.3816835,0.57072 -0.07639,0.032 -0.156865,0.0468 -0.235897,0.0468 z", + "stroke-width": 0 + }) +); +const volumeBoostOffSVG = createSVGElement( + "svg", + { + fill: "white", + height: "24px", + stroke: "white", + viewBox: "0 0 24 24", + width: "24px" + }, + createSVGElement("path", { + d: "m 12.09635,16.420143 c -0.237177,0 -0.474314,-0.0902 -0.654657,-0.27176 -0.361886,-0.36192 -0.361886,-0.94872 0,-1.31064 1.564971,-1.56512 1.564971,-4.11228 0,-5.6774055 -0.361886,-0.361955 -0.361886,-0.9487194 0,-1.3106594 0.361925,-0.36194 0.948629,-0.36194 1.310515,0 2.287503,2.2877849 2.287503,6.0097049 0,8.2987049 -0.180343,0.18036 -0.41748,0.27176 -0.654618,0.27176 z", + "stroke-width": 0 + }), + createSVGElement("path", { + d: "m 9.5297155,20.647343 c -0.160584,0 -0.318688,-0.063 -0.437243,-0.18036 L 4.3321986,15.706143 H 2.1175737 c -0.3408996,0 -0.6175732,-0.27668 -0.6175732,-0.61764 V 8.9120255 c 0,-0.340933 0.2766736,-0.6176434 0.6175732,-0.6176434 h 2.2146249 l 4.7602739,-4.760824 c 0.176625,-0.176644 0.442162,-0.229764 0.67314,-0.133408 0.230978,0.096358 0.3816835,0.321176 0.3816835,0.570704 V 20.029663 c 0,0.24956 -0.1507055,0.4756 -0.3816835,0.57072 -0.07639,0.032 -0.156865,0.0468 -0.235897,0.0468 z", + "stroke-width": 0 }) ); const openTranscriptSVG = createSVGElement( @@ -140,20 +153,20 @@ const maximizePlayerSVG = createSVGElement( "svg", { fill: "none", - height: "100%", + height: "24px", stroke: "white", "stroke-width": "1.5", - viewBox: "0 0 36 36", - width: "100%" + viewBox: "0 0 24 24", + width: "24px" }, createSVGElement("path", { - 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", + d: "M 21.788344,21.788346 H 2.2116561 V 2.2116538 H 21.788344 Z m -19.5766878,0 V 2.2116538 H 21.788344 V 21.788346 Z", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "1.5" }), createSVGElement("path", { - d: "m 18,14.497768 v 7.004464 M 21.502231,18 h -7.004462", + d: "m 12.000002,7.804995 v 8.39001 m 4.195002,-4.195006 H 7.8049961", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "1.5" @@ -163,20 +176,20 @@ const minimizePlayerSVG = createSVGElement( "svg", { fill: "none", - height: "100%", + height: "24px", stroke: "white", "stroke-width": "1.5", - viewBox: "0 0 36 36", - width: "100%" + viewBox: "0 0 24 24", + width: "24px" }, createSVGElement("path", { - 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", + d: "M 21.788346,21.788346 H 2.2116542 V 2.2116541 H 21.788346 Z m -19.5766917,0 V 2.2116541 H 21.788346 V 21.788346 Z", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "1.5" }), createSVGElement("path", { - d: "M 21.502231,18 H 14.497769", + d: "M 16.195005,12 H 7.804995", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "1.5" @@ -185,36 +198,35 @@ const minimizePlayerSVG = createSVGElement( export const featureIcons = { loopButton: { feature_menu: loopOnSVG, - shared_position_icon: { + shared_icon_position: { off: loopOffSVG, on: loopOnSVG } }, maximizePlayerButton: { feature_menu: maximizePlayerSVG, - shared_position_icon: { + shared_icon_position: { off: maximizePlayerSVG, on: minimizePlayerSVG } }, openTranscriptButton: { feature_menu: openTranscriptSVG, - shared_position_icon: openTranscriptSVG + shared_icon_position: openTranscriptSVG }, screenshotButton: { feature_menu: screenshotButtonSVG, - shared_position_icon: screenshotButtonSVG + shared_icon_position: screenshotButtonSVG }, volumeBoostButton: { - feature_menu: volumeBoostSVG, - shared_position_icon: { - off: volumeBoostSVG, // TODO: replace with different icon - on: volumeBoostSVG + feature_menu: volumeBoostOnSVG, + shared_icon_position: { + off: volumeBoostOffSVG, + on: volumeBoostOnSVG } } } satisfies FeatureIconsType; -// TODO: finish moving icon definitions to here -export function getIcon( +export function getFeatureIcon( featureName: Name, placement: GetPlacementKey ) { diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 5987ccc5..10ce9efa 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -2,7 +2,8 @@ import type { ExtensionSendOnlyMessageMappings, Messages, YouTubePlayerDiv } fro import { featureButtonFunctions } from "@/src/features"; import { automaticTheaterMode } from "@/src/features/automaticTheaterMode"; -import { checkIfFeatureButtonExists } from "@/src/features/buttonPlacement/utils"; +import { featuresInControls } from "@/src/features/buttonPlacement"; +import { checkIfFeatureButtonExists, getFeatureButton, updateFeatureButtonTitle } from "@/src/features/buttonPlacement/utils"; import { disableCustomCSS, enableCustomCSS } from "@/src/features/customCSS"; import { customCSSExists, updateCustomCSS } from "@/src/features/customCSS/utils"; import { enableFeatureMenu, setupFeatureMenuEventListeners } from "@/src/features/featureMenu"; @@ -23,8 +24,15 @@ import { addScreenshotButton, removeScreenshotButton } from "@/src/features/scre import adjustSpeedOnScrollWheel from "@/src/features/scrollWheelSpeedControl"; import adjustVolumeOnScrollWheel from "@/src/features/scrollWheelVolumeControl"; import { promptUserToResumeVideo, setupVideoHistory } from "@/src/features/videoHistory"; -import volumeBoost, { addVolumeBoostButton, disableVolumeBoost, enableVolumeBoost, removeVolumeBoostButton } from "@/src/features/volumeBoost"; +import volumeBoost, { + addVolumeBoostButton, + applyVolumeBoost, + disableVolumeBoost, + enableVolumeBoost, + removeVolumeBoostButton +} from "@/src/features/volumeBoost"; import { i18nService } from "@/src/i18n"; +import { type ToggleFeatures, toggleFeatures } from "@/src/icons"; import eventManager from "@/utils/EventManager"; import { browserColorLog, @@ -297,9 +305,27 @@ window.addEventListener("DOMContentLoaded", function () { data: { language } } = message; window.i18nextInstance = await i18nService(language); - updateFeatureMenuTitle(window.i18nextInstance.t("pages.content.features.featureMenu.label")); - for (const feature of featuresInMenu) { - updateFeatureMenuItemLabel(feature, window.i18nextInstance.t(`pages.content.features.${feature}.label`)); + if (featuresInMenu.size > 0) { + updateFeatureMenuTitle(window.i18nextInstance.t("pages.content.features.featureMenu.label")); + for (const feature of featuresInMenu) { + updateFeatureMenuItemLabel(feature, window.i18nextInstance.t(`pages.content.features.${feature}.label`)); + } + } + if (featuresInControls.size > 0) { + for (const feature of featuresInControls) { + if (toggleFeatures.includes(feature)) { + const toggleFeature = feature as ToggleFeatures; + const featureButton = getFeatureButton(toggleFeature); + if (!featureButton) return; + const buttonChecked = JSON.parse(featureButton.ariaChecked ?? "false") as boolean; + updateFeatureButtonTitle( + feature, + window.i18nextInstance.t(`pages.content.features.${toggleFeature}.toggle.${buttonChecked ? "on" : "off"}`) + ); + } else { + updateFeatureButtonTitle(feature, window.i18nextInstance.t(`pages.content.features.${feature}.label`)); + } + } } break; } @@ -367,10 +393,10 @@ window.addEventListener("DOMContentLoaded", function () { const { data: { buttonPlacement: buttonPlacements } } = message; - for (const [featureName, buttonPlacement] of Object.entries(buttonPlacements)) { - const buttonExists = checkIfFeatureButtonExists(featureName, buttonPlacement); + for (const [featureName, { new: newPlacement, old: oldPlacement }] of Object.entries(buttonPlacements)) { + const buttonExists = checkIfFeatureButtonExists(featureName, newPlacement); if (buttonExists) continue; - featureButtonFunctions[featureName].remove(); + featureButtonFunctions[featureName].remove(oldPlacement); await featureButtonFunctions[featureName].add(); } break; @@ -401,6 +427,6 @@ window.addEventListener("error", (event) => { window.addEventListener("unhandledrejection", (event) => { event.preventDefault(); - const errorLine = event.reason instanceof Error ? event.reason.stack : "Stack trace not available"; + const errorLine = event.reason instanceof Error ? event?.reason?.stack : "Stack trace not available"; browserColorLog(`Unhandled rejection: ${event.reason}\nAt: ${errorLine}`, "FgRed"); }); diff --git a/src/pages/inject/index.tsx b/src/pages/inject/index.tsx index b7dc5b5d..bca4430f 100644 --- a/src/pages/inject/index.tsx +++ b/src/pages/inject/index.tsx @@ -1,7 +1,17 @@ import type { AvailableLocales } from "@/src/i18n"; -import type { ContentSendOnlyMessageMappings, Messages, RememberedVolumes, StorageChanges, configuration, configurationKeys } from "@/src/types"; import { getVideoHistory, setVideoHistory } from "@/src/features/videoHistory/utils"; +import { + type ButtonPlacement, + type ContentSendOnlyMessageMappings, + type FeaturesThatHaveButtons, + type Messages, + type RememberedVolumes, + type StorageChanges, + type configuration, + type configurationKeys, + featuresThatHaveButtons +} from "@/src/types"; import { defaultConfiguration } from "@/src/utils/constants"; import { parseStoredValue, sendExtensionMessage, sendExtensionOnlyMessage } from "@/src/utils/utilities"; @@ -187,106 +197,117 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = // Get the current configuration options from local storage const options = await getStoredSettings(); const keyActions: { - [K in keyof configuration]?: (newValue: configuration[K]) => void; + [K in keyof configuration]?: (oldValue: configuration[K], newValue: configuration[K]) => void; } = { - button_placements: (newValue) => { + button_placements: (oldValue, newValue) => { sendExtensionOnlyMessage("buttonPlacementChange", { - buttonPlacement: newValue + buttonPlacement: featuresThatHaveButtons.reduce( + (acc, feature) => { + const { [feature]: oldPlacement } = oldValue; + const { [feature]: newPlacement } = newValue; + return Object.assign(acc, { + [feature]: { + new: newPlacement, + old: oldPlacement + } + }); + }, + {} as Record + ) }); }, - custom_css_code: (newValue) => { + custom_css_code: (__oldValue, newValue) => { sendExtensionOnlyMessage("customCSSChange", { customCSSCode: newValue, customCSSEnabled: options.enable_custom_css }); }, - enable_automatic_theater_mode: (newValue) => { + enable_automatic_theater_mode: (__oldValue, newValue) => { sendExtensionOnlyMessage("automaticTheaterModeChange", { automaticTheaterModeEnabled: newValue }); }, - enable_custom_css: (newValue) => { + enable_custom_css: (__oldValue, newValue) => { sendExtensionOnlyMessage("customCSSChange", { customCSSCode: options.custom_css_code, customCSSEnabled: newValue }); }, - enable_forced_playback_speed: (newValue) => { + enable_forced_playback_speed: (__oldValue, newValue) => { sendExtensionOnlyMessage("playerSpeedChange", { enableForcedPlaybackSpeed: newValue, playerSpeed: options.player_speed }); }, - enable_hide_scrollbar: (newValue) => { + enable_hide_scrollbar: (__oldValue, newValue) => { sendExtensionOnlyMessage("hideScrollBarChange", { hideScrollBarEnabled: newValue }); }, - enable_loop_button: (newValue) => { + enable_loop_button: (__oldValue, newValue) => { sendExtensionOnlyMessage("loopButtonChange", { loopButtonEnabled: newValue }); }, - enable_maximize_player_button: (newValue) => { + enable_maximize_player_button: (__oldValue, newValue) => { sendExtensionOnlyMessage("maximizeButtonChange", { maximizePlayerButtonEnabled: newValue }); }, - enable_open_transcript_button: (newValue) => { + enable_open_transcript_button: (__oldValue, newValue) => { sendExtensionOnlyMessage("openTranscriptButtonChange", { openTranscriptButtonEnabled: newValue }); }, - enable_open_youtube_settings_on_hover: (newValue) => { + enable_open_youtube_settings_on_hover: (__oldValue, newValue) => { sendExtensionOnlyMessage("openYTSettingsOnHoverChange", { openYouTubeSettingsOnHoverEnabled: newValue }); }, - enable_remaining_time: (newValue) => { + enable_remaining_time: (__oldValue, newValue) => { sendExtensionOnlyMessage("remainingTimeChange", { remainingTimeEnabled: newValue }); }, - enable_remember_last_volume: (newValue) => { + enable_remember_last_volume: (__oldValue, newValue) => { sendExtensionOnlyMessage("rememberVolumeChange", { rememberVolumeEnabled: newValue }); }, - enable_screenshot_button: (newValue) => { + enable_screenshot_button: (__oldValue, newValue) => { sendExtensionOnlyMessage("screenshotButtonChange", { screenshotButtonEnabled: newValue }); }, - enable_scroll_wheel_speed_control: (newValue) => { + enable_scroll_wheel_speed_control: (__oldValue, newValue) => { sendExtensionOnlyMessage("scrollWheelSpeedControlChange", { scrollWheelSpeedControlEnabled: newValue }); }, - enable_scroll_wheel_volume_control: (newValue) => { + enable_scroll_wheel_volume_control: (__oldValue, newValue) => { sendExtensionOnlyMessage("scrollWheelVolumeControlChange", { scrollWheelVolumeControlEnabled: newValue }); }, - enable_video_history: (newValue) => { + enable_video_history: (__oldValue, newValue) => { sendExtensionOnlyMessage("videoHistoryChange", { videoHistoryEnabled: newValue }); }, - enable_volume_boost: (newValue) => { + enable_volume_boost: (__oldValue, newValue) => { sendExtensionOnlyMessage("volumeBoostChange", { - volumeBoostAmount: options.volume_boost_amount, volumeBoostEnabled: newValue, volumeBoostMode: options.volume_boost_mode }); }, - feature_menu_open_type: (newValue) => { + feature_menu_open_type: (__oldValue, newValue) => { sendExtensionOnlyMessage("featureMenuOpenTypeChange", { featureMenuOpenType: newValue }); }, - language: (newValue) => { + language: (__oldValue, newValue) => { sendExtensionOnlyMessage("languageChange", { language: newValue }); }, - player_speed: (newValue) => { + player_speed: (__oldValue, newValue) => { sendExtensionOnlyMessage("playerSpeedChange", { enableForcedPlaybackSpeed: options.enable_forced_playback_speed, playerSpeed: newValue @@ -299,7 +320,7 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = volumeBoostMode: options.volume_boost_mode }); }, - volume_boost_mode: (newValue) => { + volume_boost_mode: (__oldValue, newValue) => { sendExtensionOnlyMessage("volumeBoostChange", { volumeBoostAmount: options.volume_boost_amount, volumeBoostEnabled: options.enable_volume_boost, @@ -310,10 +331,12 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = Object.entries(castedChanges).forEach(([key, change]) => { if (isValidChange(change)) { if (!change.newValue) return; + if (!change.oldValue) return; + const oldValue = parseStoredValue(change.oldValue) as configuration[typeof key]; const newValue = parseStoredValue(change.newValue) as configuration[typeof key]; const { [key]: handler } = keyActions; if (!handler) return; - (handler as (newValue: configuration[typeof key]) => void)(newValue); + (handler as (oldValue: configuration[typeof key], newValue: configuration[typeof key]) => void)(oldValue, newValue); } }); }; diff --git a/src/types/index.ts b/src/types/index.ts index fb85944e..7f778ee3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -45,7 +45,6 @@ export const volumeBoostMode = ["global", "per_video"] as const; export type VolumeBoostMode = (typeof volumeBoostMode)[number]; export const buttonPlacement = ["below_player", "feature_menu", "player_controls_left", "player_controls_right"] as const; export type ButtonPlacement = (typeof buttonPlacement)[number]; - export type ExtractFeatureName = T extends `pages.content.features.${infer FeatureName}.label` ? FeatureName : never; export type FeaturesThatHaveButtons = Exclude< ExtractFeatureName & `pages.content.features.${FeatureName}.label`>, @@ -143,7 +142,17 @@ export type ContentSendOnlyMessageMappings = { }; export type ExtensionSendOnlyMessageMappings = { automaticTheaterModeChange: DataResponseMessage<"automaticTheaterModeChange", { automaticTheaterModeEnabled: boolean }>; - buttonPlacementChange: DataResponseMessage<"buttonPlacementChange", { buttonPlacement: ButtonPlacementConfiguration }>; + buttonPlacementChange: DataResponseMessage< + "buttonPlacementChange", + { + buttonPlacement: { + [Key in FeaturesThatHaveButtons]: { + new: ButtonPlacement; + old: ButtonPlacement; + }; + }; + } + >; customCSSChange: DataResponseMessage<"customCSSChange", { customCSSCode: string; customCSSEnabled: boolean }>; featureMenuOpenTypeChange: DataResponseMessage<"featureMenuOpenTypeChange", { featureMenuOpenType: FeatureMenuOpenType }>; hideScrollBarChange: DataResponseMessage<"hideScrollBarChange", { hideScrollBarEnabled: boolean }>; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 06b08a98..be2d1541 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,4 @@ -import z, { ZodEnum } from "zod"; +import z, { ZodEnum, ZodObject } from "zod"; import type { ButtonPlacement, FeaturesThatHaveButtons, TypeToPartialZodSchema, configuration } from "../types"; @@ -68,8 +68,16 @@ export const defaultConfiguration = { volume_boost_amount: 1, volume_boost_mode: "global" } satisfies configuration; -// TODO: fix this type error -export const configurationImportSchema: TypeToPartialZodSchema = z.object({ +export const configurationImportSchema: TypeToPartialZodSchema< + configuration, + "button_placements", + { + button_placements: ZodObject<{ + [K in FeaturesThatHaveButtons]: ZodEnum<[ButtonPlacement]>; + }>; + }, + true +> = z.object({ button_placements: z.object({ ...featuresThatHaveButtons.reduce( (acc, featureName) => ({ ...acc, [featureName]: z.enum(buttonPlacement).optional() }), diff --git a/src/utils/utilities.ts b/src/utils/utilities.ts index c735bec4..21e5d725 100644 --- a/src/utils/utilities.ts +++ b/src/utils/utilities.ts @@ -323,7 +323,7 @@ export function isShortsPage() { } export function formatError(error: unknown) { if (error instanceof Error) { - return `${error.message}\n${error.stack}`; + return `${error.message}\n${error?.stack}`; } else if (error instanceof String) { return error.toString(); } else { @@ -456,10 +456,10 @@ export function createTooltip({ id, text }: { - direction?: "down" | "up"; + direction?: "down" | "left" | "right" | "up"; element: HTMLElement; featureName: FeatureName; - id: string; + id: `yte-feature-${FeatureName}-tooltip`; text?: string; }): { listener: () => void; @@ -467,17 +467,38 @@ export function createTooltip({ 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 = direction === "up" ? `${rect.top - 2}px` : `${rect.bottom + 32}px`; - tooltip.style.zIndex = "99999"; + // Create tooltip element + const tooltip = createStyledElement({ + classlist: ["yte-button-tooltip", "ytp-tooltip", "ytp-rounded-tooltip", "ytp-bottom"], + elementId: id, + elementType: "div", + styles: { + ...conditionalStyles({ + condition: direction === "down" || direction === "up", + left: `${rect.left + rect.width / 2}px` + }), + ...conditionalStyles({ + condition: direction === "up", + top: `${rect.top - 2}px` + }), + ...conditionalStyles({ + condition: direction === "down", + top: `${rect.bottom + rect.height}px` + }), + ...conditionalStyles({ + condition: direction === "left", + left: `${rect.left - rect.width}px`, + top: `${rect.bottom}px` + }), + ...conditionalStyles({ + condition: direction === "right", + left: `${rect.right + rect.width}px`, + top: `${rect.bottom}px` + }), + zIndex: "99999" + } + }); const { dataset: { title } } = element; @@ -511,10 +532,17 @@ export function createTooltip({ } }; } - +export function removeTooltip(id: `yte-feature-${FeatureName}-tooltip`) { + const tooltip = document.getElementById(id); + if (!tooltip) return; + tooltip.remove(); +} export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +export function conditionalStyles(...input: ({ condition: boolean } & Partial)[]) { + return input.reduce((acc, { condition, ...style }) => (condition ? { ...acc, ...style } : acc), {} as Partial); +} // Utility function to create and style an element export function createStyledElement({ classlist,