Skip to content

Commit

Permalink
still needs some work
Browse files Browse the repository at this point in the history
  • Loading branch information
VampireChicken12 committed Jan 20, 2024
1 parent 16dbcf4 commit 34601c0
Show file tree
Hide file tree
Showing 19 changed files with 708 additions and 233 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"editor.codeActionsOnSave": {
"source.fixAll": "always"
},
"eslint.codeActionsOnSave.mode": "all"
"eslint.codeActionsOnSave.mode": "all",
"editor.snippets.codeActions.enabled": true
}
31 changes: 29 additions & 2 deletions src/components/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,34 @@ export default function Settings() {
(key: Path<configuration>) =>
({ currentTarget: { value } }: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFirstLoad(false);
setSettings((state) => (state ? { ...state, [key]: value } : undefined));
setSettings((state) => {
if (!state) {
return undefined;
}

const updatedState = { ...state };
const keys = key.split(".") as Array<keyof configuration>;
let parentValue: any = updatedState;

for (const currentKey of keys.slice(0, -1)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
({ [currentKey]: parentValue } = parentValue);
}

const propertyName = keys.at(keys.length - 1);
if (!propertyName) return updatedState;
if (typeof parentValue === "object" && parentValue !== null) {
// If the path represents a nested property, update the nested property
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
parentValue[propertyName] = value;
} else {
// If the path represents a top-level property, update it directly
// @ts-expect-error not sure how to type this
updatedState[propertyName] = value;
}

return updatedState;
});
};
const getSelectedOption = <K extends Path<configuration>>(key: K) => getPathValue(settings, key);
function saveOptions() {
Expand Down Expand Up @@ -469,7 +496,7 @@ export default function Settings() {
<SettingSection>
<SettingTitle title={t("settings.sections.featureMenu.openType.title")} />
<Setting
disabled={Object.values(settings.button_placements).some((v) => v !== "feature_menu")}
disabled={Object.values(settings.button_placements).every((v) => v !== "feature_menu")}
id="feature_menu_open_type"
label={t("settings.sections.featureMenu.openType.select.label")}
onChange={setValueOption("feature_menu_open_type")}
Expand Down
55 changes: 54 additions & 1 deletion src/features/buttonPlacement/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,54 @@
// TODO: Code button placement code for below player, left controls and right controls
import type { GetIconType } from "@/src/icons";
import type { ButtonPlacement, FeaturesThatHaveButtons } from "@/src/types";

import { waitForSpecificMessage } from "@/src/utils/utilities";

import { addFeatureItemToMenu, removeFeatureItemFromMenu } from "../featureMenu/utils";
import { type ListenerType, makeFeatureButton, placeButton } from "./utils";

export async function addFeatureButton<
Name extends FeaturesThatHaveButtons,
Placement extends ButtonPlacement,
Label extends string,
Toggle extends boolean
>(featureName: Name, placement: Placement, label: Label, icon: GetIconType<Name, Placement>, listener: ListenerType<Toggle>, isToggle: boolean) {
switch (placement) {
case "feature_menu": {
if (icon instanceof SVGSVGElement) await addFeatureItemToMenu(featureName, label, icon, listener, isToggle);
break;
}
case "below_player":
case "player_controls_left":
case "player_controls_right": {
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) {
case "feature_menu": {
removeFeatureItemFromMenu(featureName);
break;
}
case "below_player":
case "player_controls_left":
case "player_controls_right": {
const button = document.querySelector<HTMLButtonElement>(`#yte-feature-${featureName}-button`);
if (!button) return;
button.remove();
break;
}
}
}
151 changes: 151 additions & 0 deletions src/features/buttonPlacement/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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 boolean> = Toggle extends true ? (checked?: boolean) => void : () => void;

function buttonClickListener<Placement extends ButtonPlacement, Name extends FeaturesThatHaveButtons, Toggle extends boolean>(
button: HTMLButtonElement,
icon: GetIconType<Name, Placement>,
listener: ListenerType<Toggle>,
isToggle: boolean
) {
if (isToggle) {
button.ariaChecked = button.ariaChecked ? (!JSON.parse(button.ariaChecked)).toString() : "false";
// TODO: add language strings for toggle features and update the tooltip text
if (typeof icon === "object" && "off" in icon && "on" in icon) {
updateFeatureButtonIcon(button, JSON.parse(button.ariaChecked) ? icon.on : icon.off);
} else if (typeof icon === "object" && icon instanceof SVGSVGElement) {
updateFeatureButtonIcon(button, icon);
}
listener(JSON.parse(button.ariaChecked) as boolean);
} else {
listener();
}
}

export function makeFeatureButton<Name extends FeaturesThatHaveButtons, Placement extends ButtonPlacement, Toggle extends boolean>(
featureName: Name,
placement: Placement,
label: string,
icon: GetIconType<Name, Placement>,
listener: ListenerType<Toggle>,
isToggle: boolean
) {
if (placement === "feature_menu") throw new Error("Cannot make a feature button for the feature menu");
const buttonExists = document.querySelector(`button#${getFeatureButtonId(featureName)}`) !== null;
// TODO: fix right controls button making control buttons overflow
const button = createStyledElement({
classlist: ["ytp-button"],
elementId: `${getFeatureButtonId(featureName)}`,
elementType: "button",
styles: {
alignContent: "center",
display: "flex",
flexWrap: "wrap",
height: "48px",
justifyContent: "center",
padding: "0px 4px",
width: "48px"
}
});
const { listener: tooltipListener } = createTooltip({
direction: placement === "below_player" ? "up" : "up",
element: button,
featureName,
id: `yte-feature-${featureName}-tooltip`,
text: label
});
if (buttonExists) {
eventManager.removeEventListener(button, "click", featureName);
eventManager.addEventListener(button, "click", () => buttonClickListener<Placement, Name, Toggle>(button, icon, listener, isToggle), 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) {
button.append(icon);
}
}

eventManager.addEventListener(button, "mouseover", tooltipListener, featureName);
eventManager.addEventListener(button, "click", () => buttonClickListener<Placement, Name, Toggle>(button, icon, listener, isToggle), featureName);
return button;
}
function updateFeatureButtonIcon(button: HTMLButtonElement, icon: SVGElement) {
if (button.firstChild) {
button.firstChild.remove();
button.append(icon);
}
}
export function placeButton(button: HTMLButtonElement, placement: Exclude<ButtonPlacement, "feature_menu">) {
switch (placement) {
case "below_player": {
const player = document.querySelector<HTMLDivElement>("div#primary > div#primary-inner > div#player");
if (!player) return;
const buttonContainerExists = document.querySelector<HTMLDivElement>(`#${buttonContainerId}`) !== null;
const buttonContainer = createStyledElement({
elementId: buttonContainerId,
elementType: "div",
styles: {
display: "flex",
justifyContent: "center"
}
});
buttonContainer.append(button);
if (buttonContainerExists) return;
player.insertAdjacentElement("afterend", buttonContainer);
break;
}
case "player_controls_left": {
const leftControls = document.querySelector<HTMLDivElement>(".ytp-left-controls");
if (!leftControls) return;
leftControls.append(button);
break;
}
case "player_controls_right": {
const rightControls = document.querySelector<HTMLDivElement>(".ytp-right-controls");
if (!rightControls) return;
rightControls.prepend(button);
break;
}
}
}
export function checkIfFeatureButtonExists(featureName: FeaturesThatHaveButtons, placement: ButtonPlacement): boolean {
switch (placement) {
case "below_player": {
const buttonContainer = document.querySelector<HTMLDivElement>(`#${buttonContainerId}`);
if (!buttonContainer) return false;
return buttonContainer.querySelector<HTMLButtonElement>(`#${getFeatureButtonId(featureName)}`) !== null;
}
case "player_controls_left": {
const leftControls = document.querySelector<HTMLDivElement>(".ytp-left-controls");
if (!leftControls) return false;
return leftControls.querySelector<HTMLButtonElement>(`#${getFeatureButtonId(featureName)}`) !== null;
}
case "player_controls_right": {
const rightControls = document.querySelector<HTMLDivElement>(".ytp-right-controls");
if (!rightControls) return false;
return rightControls.querySelector<HTMLButtonElement>(`#${getFeatureButtonId(featureName)}`) !== null;
}
case "feature_menu": {
const featureMenu = document.querySelector<HTMLDivElement>("#yte-feature-menu");
if (!featureMenu) return false;
return featureMenu.querySelector<HTMLDivElement>(`#${getFeatureIds(featureName).featureMenuItemId}`) !== null;
}
}
}
export function getFeatureButtonId(featureName: FeaturesThatHaveButtons) {
return `yte-feature-${featureName}-button` as const;
}
export const buttonContainerId = "yte-button-container";
43 changes: 20 additions & 23 deletions src/features/featureMenu/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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<FeaturesThatHaveButtons>();

function featureMenuClickListener(menuItem: HTMLDivElement, listener: (checked?: boolean) => void, isToggle: boolean) {
function featureMenuClickListener<Toggle extends boolean = false>(menuItem: HTMLDivElement, listener: ListenerType<Toggle>, isToggle: boolean) {
if (isToggle) {
menuItem.ariaChecked = menuItem.ariaChecked ? (!JSON.parse(menuItem.ariaChecked)).toString() : "false";
listener(JSON.parse(menuItem.ariaChecked) as boolean);
Expand All @@ -15,25 +18,19 @@ function featureMenuClickListener(menuItem: HTMLDivElement, listener: (checked?:
}
/**
* 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.
* @param featureName The name of the feature
* @param label The label for the feature
* @param icon The icon for the feature
* @param listener The listener for the feature
* @param isToggle Whether the feature is a toggle
*/
export async function addFeatureItemToMenu({
featureName,
icon,
isToggle = false,
label,
listener
}: {
featureName: FeaturesThatHaveButtons;
icon: SVGElement;
isToggle?: boolean;
label: string;
listener: (checked?: boolean) => void;
}) {
export async function addFeatureItemToMenu<Name extends FeaturesThatHaveButtons, Toggle extends boolean>(
featureName: Name,
label: string,
icon: BasicIcon,
listener: ListenerType<Toggle>,
isToggle: boolean
) {
// Add the feature name to the set of features in the menu
featuresInMenu.add(featureName);

Expand All @@ -45,7 +42,7 @@ export async function addFeatureItemToMenu({
if (!featureMenu) return;

// Check if the feature item already exists in the menu
const featureExistsInMenu = featureMenu.querySelector<HTMLDivElement>(`#yte-feature-${featureName}`);
const featureExistsInMenu = featureMenu.querySelector<HTMLDivElement>(`#${getFeatureIds(featureName).featureMenuItemId}`);
if (featureExistsInMenu) {
const menuItem = getFeatureMenuItem(featureName);
if (!menuItem) return;
Expand Down Expand Up @@ -180,7 +177,7 @@ export function getFeatureIds(featureName: FeatureName): {
featureMenuItemLabelId: FeatureMenuItemLabelId;
} {
const featureMenuItemIconId: FeatureMenuItemIconId = `yte-${featureName}-icon`;
const featureMenuItemId: FeatureMenuItemId = `yte-feature-${featureName}`;
const featureMenuItemId: FeatureMenuItemId = `yte-feature-${featureName}-menuitem`;
const featureMenuItemLabelId: FeatureMenuItemLabelId = `yte-${featureName}-label`;
return {
featureMenuItemIconId,
Expand All @@ -197,6 +194,6 @@ export function getFeatureMenuItemLabel(featureName: FeatureName): HTMLDivElemen
return document.querySelector(selector);
}
export function getFeatureMenuItem(featureName: FeatureName): HTMLDivElement | null {
const selector: WithId<FeatureMenuItemId> = `#yte-feature-${featureName}`;
return document.querySelector(selector);
const selector: WithId<FeatureMenuItemId> = `#yte-feature-${featureName}-menuitem`;
return document.querySelector(`#yte-panel-menu > ${selector}`);
}
36 changes: 36 additions & 0 deletions src/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { FeaturesThatHaveButtons } from "../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";

export const featureButtonFunctions = {
loopButton: {
add: addLoopButton,
remove: removeLoopButton
},
maximizePlayerButton: {
add: addMaximizePlayerButton,
remove: removeMaximizePlayerButton
},
openTranscriptButton: {
add: addOpenTranscriptButton,
remove: removeOpenTranscriptButton
},
screenshotButton: {
add: addScreenshotButton,
remove: removeScreenshotButton
},
volumeBoostButton: {
add: addVolumeBoostButton,
remove: removeVolumeBoostButton
}
} satisfies Record<
FeaturesThatHaveButtons,
{
add: (() => Promise<void>) | (() => void);
remove: (() => Promise<void>) | (() => void);
}
>;
Loading

0 comments on commit 34601c0

Please sign in to comment.