Procházet zdrojové kódy

feat: add circular button component to interface

Sv443 před 11 měsíci
rodič
revize
43afd758e7

+ 53 - 18
contributing.md

@@ -5,20 +5,16 @@ If you have any questions or need help, feel free to contact me, [see my homepag
 
 
 <br>
 <br>
 
 
-- [BetterYTM - Contributing Guide](#betterytm---contributing-guide)
-  - [Submitting translations:](#submitting-translations)
-    - [Adding translations for a new language:](#adding-translations-for-a-new-language)
-    - [Editing an existing translation:](#editing-an-existing-translation)
-  - [Setting up the project for local development:](#setting-up-the-project-for-local-development)
-    - [Requirements:](#requirements)
-    - [These are the CLI commands available after setting up the project:](#these-are-the-cli-commands-available-after-setting-up-the-project)
-    - [Extras:](#extras)
-  - [Developing a plugin that interfaces with BetterYTM:](#developing-a-plugin-that-interfaces-with-betterytm)
-    - [Example:](#example)
-    - [Basic format:](#basic-format)
-    - [Practical Example:](#practical-example)
-  - [Shimming for TypeScript without errors \& with autocomplete:](#shimming-for-typescript-without-errors--with-autocomplete)
-  - [Global functions:](#global-functions)
+- [Submitting translations](#submitting-translations)
+  - [Adding translations for a new language](#adding-translations-for-a-new-language)
+  - [Editing an existing translation](#editing-an-existing-translation)
+- [Setting up the project for local development](#setting-up-the-project-for-local-development)
+  - [Requirements](#requirements)
+  - [CLI commands](#these-are-the-cli-commands-available-after-setting-up-the-project)
+  - [Extras](#extras)
+- [Developing a plugin that interfaces with BetterYTM](#developing-a-plugin-that-interfaces-with-betterytm)
+  - [Shimming for TypeScript without errors & with autocomplete](#shimming-for-typescript-without-errors--with-autocomplete)
+  - [Global functions and classes on the plugin interface](#global-functions-and-classes)
 
 
 <br><br>
 <br><br>
 
 
@@ -258,8 +254,8 @@ An easy way to do this might be to include BetterYTM as a Git submodule, as long
 
 
 <!-- #region global interface functions -->
 <!-- #region global interface functions -->
 
 
-### Global functions:
-These are the global functions that are exposed by BetterYTM through the `unsafeWindow.BYTM` object.  
+### Global functions and classes:
+These are the global functions and classes that are exposed by BetterYTM through the `unsafeWindow.BYTM` object.  
 The usage and example blocks on each are written in TypeScript but can be used in JavaScript as well, after removing all type annotations.  
 The usage and example blocks on each are written in TypeScript but can be used in JavaScript as well, after removing all type annotations.  
   
   
 - Meta:
 - Meta:
@@ -274,6 +270,7 @@ The usage and example blocks on each are written in TypeScript but can be used i
   - [BytmDialog](#bytmdialog) - A class for creating and managing dialogs
   - [BytmDialog](#bytmdialog) - A class for creating and managing dialogs
   - [createHotkeyInput()](#createhotkeyinput) - Creates a hotkey input element
   - [createHotkeyInput()](#createhotkeyinput) - Creates a hotkey input element
   - [createToggleInput()](#createtoggleinput) - Creates a toggle input element
   - [createToggleInput()](#createtoggleinput) - Creates a toggle input element
+  - [createCircularBtn()](#createcircularbtn) - Creates a generic, circular button element
 - Translations:
 - Translations:
   - [setLocale()](#setlocale) - Sets the locale for BetterYTM
   - [setLocale()](#setlocale) - Sets the locale for BetterYTM
   - [getLocale()](#getlocale) - Returns the currently set locale
   - [getLocale()](#getlocale) - Returns the currently set locale
@@ -291,8 +288,8 @@ The usage and example blocks on each are written in TypeScript but can be used i
   - [sanitizeSong()](#sanitizesong) - Sanitizes the specified song title string to be used in fetching a lyrics URL
   - [sanitizeSong()](#sanitizesong) - Sanitizes the specified song title string to be used in fetching a lyrics URL
 - Other:
 - Other:
   - [NanoEmitter](#nanoemitter) - Abstract class for creating lightweight, type safe event emitting classes
   - [NanoEmitter](#nanoemitter) - Abstract class for creating lightweight, type safe event emitting classes
-  - [compareVersions](#compareversions) - Crudely compares two semver version strings and returns which one is newer
-  - [compareVersionArrays](#compareversionarrays) - Crudely compares two semver version number arrays and returns which one is newer
+  - [compareVersions()](#compareversions) - Crudely compares two semver version strings and returns which one is newer
+  - [compareVersionArrays()](#compareversionarrays) - Crudely compares two semver version number arrays and returns which one is newer
 
 
 <br><br>
 <br><br>
 
 
@@ -1106,6 +1103,44 @@ The usage and example blocks on each are written in TypeScript but can be used i
 
 
 <br>
 <br>
 
 
+> #### createCircularBtn()
+> Usage:
+> ```ts
+> unsafeWindow.BYTM.createCircularBtn(btnProps: {
+>   title: string,
+>   // either resourceName or src has to be specified:
+>   resourceName: string | undefined,
+>   src: string | undefined,
+>   // either href or onClick has to be specified:
+>   href: string | undefined,
+>   onClick: (event: MouseEvent | KeyboardEvent) => void | undefined,
+> }): HTMLElement
+> ```
+>
+> Creates a circular button element that can be used to trigger an action or navigate to a different page.  
+> - `title` - The title of the button that is displayed when hovering over it. Also used as a description for accessibility.
+> - `resourceName` - The name of the resource to use as the button icon (`src` can't be specified).
+> - `src` - The URL of the image to use as the button icon (`resourceName` can't be specified).
+> - `href` - The URL to navigate to when the button is interacted with (`onClick` can't be specified).
+> - `onClick` - The function that is called when the button is clicked or interacted with (`href` can't be specified).
+>
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+>
+> ```ts
+> const circularBtn = unsafeWindow.BYTM.createCircularBtn({
+>   title: "My cool button",
+>   resourceName: "icon-help",
+>   onClick() {
+>     console.log("The button was clicked");
+>   },
+> });
+> 
+> document.querySelector("#my-element").appendChild(circularBtn);
+> ```
+> </details>
+
+<br>
+
 > ### compareVersions()
 > ### compareVersions()
 > Usage:
 > Usage:
 > ```ts
 > ```ts

+ 20 - 12
src/components/genericButton.ts → src/components/circularButton.ts

@@ -1,12 +1,19 @@
 import { getResourceUrl, onInteraction } from "../utils";
 import { getResourceUrl, onInteraction } from "../utils";
 import type { ResourceKey } from "../types";
 import type { ResourceKey } from "../types";
 
 
-type CreateGenericBtnOptions = {
-  /** Resource key for the button icon */
-  resourceName: ResourceKey | "_";
-  /** Tooltip and aria-label of the button */
-  title: string;
-}
+type CircularBtnOptions = (
+  | {
+    /** Resource key for the button icon */
+    resourceName: ResourceKey | "_";
+    /** Tooltip and aria-label of the button */
+    title: string;
+  }
+  | {
+    src: string;
+    /** Tooltip and aria-label of the button */
+    title: string;
+  }
+)
 & (
 & (
   {
   {
     /** URL to navigate to when the button is clicked */
     /** URL to navigate to when the button is clicked */
@@ -20,16 +27,17 @@ type CreateGenericBtnOptions = {
 );
 );
 
 
 /**
 /**
- * Creates a generic button element.  
+ * Creates a generic, circular button element.  
  * If `href` is provided, the button will be an anchor element.  
  * If `href` is provided, the button will be an anchor element.  
- * If `onClick` is provided, the button will be a div element.
+ * If `onClick` is provided, the button will be a div element.  
+ * Provide either `resourceName` or `src` to specify the icon inside the button.
  */
  */
-export async function createGenericBtn({
-  resourceName,
+export async function createCircularBtn({
   title,
   title,
   href,
   href,
   onClick,
   onClick,
-}: CreateGenericBtnOptions) {
+  ...rest
+}: CircularBtnOptions) {
   let btnElem: HTMLElement;
   let btnElem: HTMLElement;
   if(href) {
   if(href) {
     btnElem = document.createElement("a");
     btnElem = document.createElement("a");
@@ -48,7 +56,7 @@ export async function createGenericBtn({
 
 
   const imgElem = document.createElement("img");
   const imgElem = document.createElement("img");
   imgElem.classList.add("bytm-generic-btn-img");
   imgElem.classList.add("bytm-generic-btn-img");
-  imgElem.src = await getResourceUrl(resourceName);
+  imgElem.src = "src" in rest ? rest.src : await getResourceUrl(rest.resourceName);
 
 
   btnElem.appendChild(imgElem);
   btnElem.appendChild(imgElem);
 
 

+ 9 - 1
src/components/hotkeyInput.ts

@@ -8,6 +8,8 @@ interface HotkeyInputProps {
   onChange: (hotkey: HotkeyObj) => void;
   onChange: (hotkey: HotkeyObj) => void;
 }
 }
 
 
+let otherHotkeyInputActive = false;
+
 const reservedKeys = ["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "Meta", "Tab", "Space", " "];
 const reservedKeys = ["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "Meta", "Tab", "Space", " "];
 
 
 /** Creates a hotkey input element */
 /** Creates a hotkey input element */
@@ -20,7 +22,7 @@ export function createHotkeyInput({ initialValue, onChange }: HotkeyInputProps):
 
 
   const infoElem = document.createElement("span");
   const infoElem = document.createElement("span");
   infoElem.classList.add("bytm-hotkey-info");
   infoElem.classList.add("bytm-hotkey-info");
-  
+
   const inputElem = document.createElement("input");
   const inputElem = document.createElement("input");
   inputElem.type = "button";
   inputElem.type = "button";
   inputElem.classList.add("bytm-ftconf-input", "bytm-hotkey-input", "bytm-btn");
   inputElem.classList.add("bytm-ftconf-input", "bytm-hotkey-input", "bytm-btn");
@@ -36,7 +38,10 @@ export function createHotkeyInput({ initialValue, onChange }: HotkeyInputProps):
   resetElem.ariaLabel = resetElem.title = t("reset");
   resetElem.ariaLabel = resetElem.title = t("reset");
 
 
   const deactivate = () => {
   const deactivate = () => {
+    if(!otherHotkeyInputActive)
+      return;
     siteEvents.emit("hotkeyInputActive", false);
     siteEvents.emit("hotkeyInputActive", false);
+    otherHotkeyInputActive = false;
     const curHk = currentHotkey ?? initialValue;
     const curHk = currentHotkey ?? initialValue;
     inputElem.value = curHk?.code ?? t("hotkey_input_click_to_change");
     inputElem.value = curHk?.code ?? t("hotkey_input_click_to_change");
     inputElem.dataset.state = "inactive";
     inputElem.dataset.state = "inactive";
@@ -45,7 +50,10 @@ export function createHotkeyInput({ initialValue, onChange }: HotkeyInputProps):
   };
   };
 
 
   const activate = () => {
   const activate = () => {
+    if(otherHotkeyInputActive)
+      return;
     siteEvents.emit("hotkeyInputActive", true);
     siteEvents.emit("hotkeyInputActive", true);
+    otherHotkeyInputActive = true;
     inputElem.value = "< ... >";
     inputElem.value = "< ... >";
     inputElem.dataset.state = "active";
     inputElem.dataset.state = "active";
     inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");
     inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");

+ 1 - 1
src/components/index.ts

@@ -1,4 +1,4 @@
 export * from "./BytmDialog";
 export * from "./BytmDialog";
-export * from "./genericButton";
+export * from "./circularButton";
 export * from "./hotkeyInput";
 export * from "./hotkeyInput";
 export * from "./toggleInput";
 export * from "./toggleInput";

+ 2 - 1
src/interface.ts

@@ -7,7 +7,7 @@ import { getFeatures, setFeatures } from "./config";
 import { compareVersionArrays, compareVersions, featInfo, fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong, type LyricsCache } from "./features";
 import { compareVersionArrays, compareVersions, featInfo, fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong, type LyricsCache } from "./features";
 import { allSiteEvents, siteEvents, type SiteEventsMap } from "./siteEvents";
 import { allSiteEvents, siteEvents, type SiteEventsMap } from "./siteEvents";
 import { LogLevel, type FeatureConfig, type FeatureInfo, type LyricsCacheEntry, type PluginDef, type PluginInfo, type PluginRegisterResult, type PluginDefResolvable, type PluginEventMap, type PluginItem, type BytmObject } from "./types";
 import { LogLevel, type FeatureConfig, type FeatureInfo, type LyricsCacheEntry, type PluginDef, type PluginInfo, type PluginRegisterResult, type PluginDefResolvable, type PluginEventMap, type PluginItem, type BytmObject } from "./types";
-import { BytmDialog, createHotkeyInput, createToggleInput } from "./components";
+import { BytmDialog, createCircularBtn, createHotkeyInput, createToggleInput } from "./components";
 
 
 const { getUnsafeWindow } = UserUtils;
 const { getUnsafeWindow } = UserUtils;
 
 
@@ -92,6 +92,7 @@ export function initInterface() {
     BytmDialog,
     BytmDialog,
     createHotkeyInput,
     createHotkeyInput,
     createToggleInput,
     createToggleInput,
+    createCircularBtn,
   };
   };
 
 
   for(const [key, value] of Object.entries(props))
   for(const [key, value] of Object.entries(props))