Browse Source

feat: add menu categories, scroll indicator & more

Sven 1 year ago
parent
commit
59434aca28
7 changed files with 298 additions and 184 deletions
  1. 0 9
      global.d.ts
  2. 51 40
      src/features/index.ts
  3. 2 3
      src/features/layout.ts
  4. 54 6
      src/features/menu/menu_old.css
  5. 182 123
      src/features/menu/menu_old.ts
  6. 0 3
      src/types.ts
  7. 9 0
      src/utils.ts

+ 0 - 9
global.d.ts

@@ -16,12 +16,3 @@ declare module "*.md" {
   const htmlContent: string;
   export default htmlContent;
 }
-
-// generic shim so TS doesn't complain *too* much
-declare global {
-  interface Window {
-    __proto__: {
-      addEventListener: (evt: string, listener: () => unknown, capture?: boolean) => void;
-    };
-  }
-}

+ 51 - 40
src/features/index.ts

@@ -6,48 +6,21 @@ export * from "./lyrics";
 export { initMenu } from "./menu/menu";
 export * from "./menu/menu_old";
 
-export type FeatInfoKeys = keyof typeof featInfo;
+/** Union of all feature keys */
+export type FeatInfoKey = keyof typeof featInfo;
+
+/** Union of all feature categories */
+export type FeatureCategory = typeof featInfo[FeatInfoKey]["category"];
+
+/** Mapping of feature category identifiers to readable strings */
+export const categoryNames: Record<FeatureCategory, string> = {
+  input: "Input",
+  layout: "Layout",
+  lyrics: "Lyrics",
+} as const;
 
 /** Contains all possible features with their default values and other config */
 export const featInfo = {
-  //#SECTION input
-  arrowKeySupport: {
-    desc: "Arrow keys to skip forwards and backwards by 10 seconds",
-    type: "toggle",
-    category: "input",
-    default: true,
-  },
-  switchBetweenSites: {
-    desc: "Add F9 as a hotkey to switch between the YT and YTM sites on a video / song",
-    type: "toggle",
-    category: "input",
-    default: true,
-  },
-  switchSitesHotkey: {
-    desc: "TODO(v1.1): Which hotkey needs to be pressed to switch sites?",
-    type: "hotkey",
-    category: "input",
-    default: {
-      key: "F9",
-      shift: false,
-      ctrl: false,
-      meta: false,
-    },
-    hidden: true,
-  },
-  disableBeforeUnloadPopup: {
-    desc: "Disable the confirmation popup that sometimes appears when trying to leave the site",
-    type: "toggle",
-    category: "input",
-    default: false,
-  },
-  anchorImprovements: {
-    desc: "TODO:FIXME: Add link elements all over the page so things can be opened in a new tab easier",
-    type: "toggle",
-    category: "input",
-    default: true,
-  },
-
   //#SECTION layout
   removeUpgradeTab: {
     desc: "Remove the Upgrade / Premium tab",
@@ -56,7 +29,7 @@ export const featInfo = {
     default: true,
   },
   volumeSliderLabel: {
-    desc: "Add a percentage label to the volume slider",
+    desc: "Add a percentage label next to the volume slider",
     type: "toggle",
     category: "layout",
     default: true,
@@ -104,6 +77,44 @@ export const featInfo = {
     unit: "s",
   },
 
+  //#SECTION input
+  arrowKeySupport: {
+    desc: "Use arrow keys to skip forwards and backwards by 10 seconds",
+    type: "toggle",
+    category: "input",
+    default: true,
+  },
+  switchBetweenSites: {
+    desc: "Add F9 as a hotkey to switch between the YT and YTM sites on a video / song",
+    type: "toggle",
+    category: "input",
+    default: true,
+  },
+  switchSitesHotkey: {
+    desc: "TODO(v1.1): Which hotkey needs to be pressed to switch sites?",
+    type: "hotkey",
+    category: "input",
+    default: {
+      key: "F9",
+      shift: false,
+      ctrl: false,
+      meta: false,
+    },
+    hidden: true,
+  },
+  disableBeforeUnloadPopup: {
+    desc: "Disable the confirmation popup that sometimes appears when trying to leave the site",
+    type: "toggle",
+    category: "input",
+    default: false,
+  },
+  anchorImprovements: {
+    desc: "TODO:FIXME: Add link elements all over the page so things can be opened in a new tab easier",
+    type: "toggle",
+    category: "input",
+    default: true,
+  },
+
   //#SECTION lyrics
   geniusLyrics: {
     desc: "Add a button to the media controls of the currently playing song to open its lyrics on genius.com",

+ 2 - 3
src/features/layout.ts

@@ -57,7 +57,7 @@ export function addWatermark() {
   const logoElem = document.querySelector("#left-content") as HTMLElement;
   insertAfter(logoElem, watermark);
 
-  log("Added watermark element", watermark);
+  log("Added watermark element");
 }
 
 /** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
@@ -149,7 +149,7 @@ export async function addConfigMenuOption(container: HTMLElement) {
 
   container.appendChild(cfgOptElem);
 
-  log("Added BYTM-Configuration button to menu popover", cfgOptElem);
+  log("Added BYTM-Configuration button to menu popover");
 }
 
 //#MARKER remove upgrade tab
@@ -231,7 +231,6 @@ function addVolumeSliderLabel(sliderElem: HTMLInputElement, sliderCont: HTMLDivE
   onSelector("#bytm-vol-slider-cont", {
     listener: (volumeCont) => {
       volumeCont.appendChild(labelElem);
-      log("Added volume slider label", labelElem);
     },
   });
 

+ 54 - 6
src/features/menu/menu_old.css

@@ -1,6 +1,7 @@
 :root {
   --bytm-menu-width: calc(min(70vw, 1000px));
   --bytm-menu-bg: #212121;
+  --bytm-menu-bg-highlight: #111111;
 }
 
 #bytm-menu-bg {
@@ -28,27 +29,34 @@
 }
 
 #bytm-menu-opts {
+  position: relative;
   max-height: 70vh;
   overflow: auto;
 }
 
 #bytm-menu-titlecont {
   display: flex;
+  justify-content: space-between;
+  padding: 8px 20px 15px 20px;
+  margin-bottom: 5px;
+  background-color: var(--bytm-menu-bg);
 }
 
 #bytm-menu-title {
   font-size: 20px;
-  margin-top: 5px;
-  margin-bottom: 8px;
+  margin: 5px 0;
 }
 
 #bytm-menu-linkscont {
   display: flex;
+  align-items: center;
 }
 
 .bytm-menu-link {
+  display: inline-flex;
+  align-items: center;
+  margin-left: 10px;
   cursor: pointer;
-  display: inline-block;
 }
 
 #bytm-menu-close {
@@ -56,14 +64,54 @@
 }
 
 #bytm-menu-footer-cont {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  margin-top: 10px;
+  padding: 32px 20px 8px 20px;
+  position: sticky;
+  bottom: 0;
   background: var(--bytm-menu-bg);
-  background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bytm-menu-bg) 18%, var(--bytm-menu-bg) 100%);
+  background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bytm-menu-bg) 25%, var(--bytm-menu-bg) 100%);
+}
+
+#bytm-menu-scroll-indicator {
+  position: sticky;
+  bottom: 60px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 32px;
+  height: 32px;
+  padding: 6px;
+  z-index: 1000;
+  background-color: var(--bytm-menu-bg-highlight);
+  border-radius: 50%;
+  cursor: pointer;
+}
+
+.bytm-ftconf-category-header {
+  font-size: 18px;
+  margin-top: 32px;
+  margin-bottom: 8px;
+  padding: 0 20px;
+}
+
+.bytm-ftconf-category-header:first-of-type {
+  margin-top: 0;
+}
+
+.betterytm-ftconf-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 20px;
 }
 
 .bytm-ftconf-label {
   user-select: none;
 }
 
-body.bytm-disable-scroll {
-  overflow-y: hidden !important;
+.bytm-ftconf-input[type=number] {
+  width: 100px;
 }

+ 182 - 123
src/features/menu/menu_old.ts

@@ -1,14 +1,15 @@
-import { debounce } from "@sv443-network/userutils";
+import { debounce, pauseFor } from "@sv443-network/userutils";
 import { defaultConfig, getFeatures, saveFeatures, setDefaultFeatures } from "../../config";
 import { scriptInfo } from "../../constants";
-import { featInfo } from "../index";
+import { FeatureCategory, FeatInfoKey, categoryNames, featInfo } from "../index";
 import { FeatureConfig } from "../../types";
-import { getResourceUrl, info, log } from "../../utils";
+import { getResourceUrl, info, isScrollable, log } from "../../utils";
 import "./menu_old.css";
 
 //#MARKER create menu elements
 
 let isMenuOpen = false;
+let scrollIndicatorShown = true;
 
 /**
  * Adds an element to open the BetterYTM menu
@@ -41,13 +42,12 @@ export async function addMenu() {
 
   //#SECTION title bar
   const titleCont = document.createElement("div");
-  titleCont.style.padding = "8px 20px 15px 20px";
-  titleCont.style.display = "flex";
-  titleCont.style.justifyContent = "space-between";
   titleCont.id = "bytm-menu-titlecont";
 
   const titleElem = document.createElement("h2");
   titleElem.id = "bytm-menu-title";
+  titleElem.role = "heading";
+  titleElem.ariaLevel = "1";
   titleElem.innerText = `${scriptInfo.name} Configuration`;
 
   const linksCont = document.createElement("div");
@@ -60,7 +60,6 @@ export async function addMenu() {
     anchorElem.target = "_blank";
     anchorElem.href = href;
     anchorElem.title = title;
-    anchorElem.style.marginLeft = "10px";
 
     const imgElem = document.createElement("img");
     imgElem.className = "bytm-menu-img";
@@ -109,144 +108,195 @@ export async function addMenu() {
     await saveFeatures(featConf);
   });
 
-  const features = getFeatures();
+  const featureCfg = getFeatures();
+  const featureCfgWithCategories = Object.entries(featInfo)
+    .reduce(
+      (acc, [key, { category }]) => {
+        if(!acc[category])
+          acc[category] = {} as Record<FeatInfoKey, unknown>;
+        acc[category][key as FeatInfoKey] = featureCfg[key as FeatInfoKey];
+        return acc;
+      },
+    {} as Record<FeatureCategory, Record<FeatInfoKey, unknown>>,
+    );
+
+  for(const category in featureCfgWithCategories) {
+    const featObj = featureCfgWithCategories[category as FeatureCategory];
+
+    const catHeaderElem = document.createElement("h3");
+    catHeaderElem.classList.add("bytm-ftconf-category-header");
+    catHeaderElem.role = "heading";
+    catHeaderElem.ariaLevel = "2";
+    catHeaderElem.innerText = `${categoryNames[category as FeatureCategory]}:`;
+    featuresCont.appendChild(catHeaderElem);
+
+    for(const featKey in featObj) {
+      const ftInfo = featInfo[featKey as keyof typeof featureCfg];
 
-  for(const key in features) {
-    const ftInfo = featInfo[key as keyof typeof features];
-
-    // @ts-ignore
-    if(!ftInfo || ftInfo.hidden === true)
-      continue;
-
-    const { desc, type, default: ftDefault } = ftInfo;
+      // @ts-ignore
+      if(!ftInfo || ftInfo.hidden === true)
+        continue;
 
-    // @ts-ignore
-    const step = ftInfo?.step ?? undefined;
-    const val = features[key as keyof typeof features];
+      const { desc, type, default: ftDefault } = ftInfo;
 
-    const initialVal = val ?? ftDefault ?? undefined;
+      // @ts-ignore
+      const step = ftInfo?.step ?? undefined;
+      const val = featureCfg[featKey as keyof typeof featureCfg];
 
-    const ftConfElem = document.createElement("div");
-    ftConfElem.id = `betterytm-ftconf-${key}`;
-    ftConfElem.style.display = "flex";
-    ftConfElem.style.flexDirection = "row";
-    ftConfElem.style.justifyContent = "space-between";
-    ftConfElem.style.padding = "8px 20px";
+      const initialVal = val ?? ftDefault ?? undefined;
 
-    {
-      const textElem = document.createElement("span");
-      textElem.style.display = "inline-block";
-      textElem.style.fontSize = "15px";
-      textElem.innerText = desc;
+      const ftConfElem = document.createElement("div");
+      ftConfElem.classList.add("betterytm-ftconf-item");
 
-      ftConfElem.appendChild(textElem);
-    }
-
-    {
-      let inputType = "text";
-      switch(type)
       {
-      case "toggle":
-        inputType = "checkbox";
-        break;
-      case "slider":
-        inputType = "range";
-        break;
-      case "number":
-        inputType = "number";
-        break;
-      }
-
-      const inputElemId = `betterytm-ftconf-${key}-input`;
-
-      const ctrlElem = document.createElement("span");
-      ctrlElem.style.display = "inline-flex";
-      ctrlElem.style.alignItems = "center";
-      ctrlElem.style.whiteSpace = "nowrap";
+        const textElem = document.createElement("span");
+        textElem.style.display = "inline-block";
+        textElem.style.fontSize = "15px";
+        textElem.innerText = desc;
 
-      const inputElem = document.createElement("input");
-      inputElem.id = inputElemId;
-      inputElem.type = inputType;
-      if(type === "toggle")
-        inputElem.style.marginLeft = "5px";
-      if(typeof initialVal !== "undefined")
-        inputElem.value = String(initialVal);
-      if(type === "number" && step)
-        inputElem.step = step;
-
-      // @ts-ignore
-      if(typeof ftInfo.min !== "undefined" && ftInfo.max !== "undefined") {
-        // @ts-ignore
-        inputElem.min = ftInfo.min;
-        // @ts-ignore
-        inputElem.max = ftInfo.max;
+        ftConfElem.appendChild(textElem);
       }
 
-      if(type === "toggle" && typeof initialVal !== "undefined")
-        inputElem.checked = Boolean(initialVal);
+      {
+        let inputType = "text";
+        switch(type)
+        {
+        case "toggle":
+          inputType = "checkbox";
+          break;
+        case "slider":
+          inputType = "range";
+          break;
+        case "number":
+          inputType = "number";
+          break;
+        }
+
+        const inputElemId = `bytm-ftconf-${featKey}-input`;
+
+        const ctrlElem = document.createElement("span");
+        ctrlElem.style.display = "inline-flex";
+        ctrlElem.style.alignItems = "center";
+        ctrlElem.style.whiteSpace = "nowrap";
+
+        const inputElem = document.createElement("input");
+        inputElem.classList.add("bytm-ftconf-input");
+        inputElem.id = inputElemId;
+        inputElem.type = inputType;
+        if(type === "toggle")
+          inputElem.style.marginLeft = "5px";
+        if(typeof initialVal !== "undefined")
+          inputElem.value = String(initialVal);
+        if(type === "number" && step)
+          inputElem.step = step;
 
-      // @ts-ignore
-      const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
+        // @ts-ignore
+        if(typeof ftInfo.min !== "undefined" && ftInfo.max !== "undefined") {
+          // @ts-ignore
+          inputElem.min = ftInfo.min;
+          // @ts-ignore
+          inputElem.max = ftInfo.max;
+        }
 
-      const fmtVal = (v: unknown) => String(v).trim();
-      const toggleLabelText = (toggled: boolean) => toggled ? "On" : "Off";
+        if(type === "toggle" && typeof initialVal !== "undefined")
+          inputElem.checked = Boolean(initialVal);
 
-      let labelElem: HTMLLabelElement | undefined;
-      if(type === "slider") {
-        labelElem = document.createElement("label");
-        labelElem.classList.add("bytm-ftconf-label");
-        labelElem.style.marginRight = "20px";
-        labelElem.style.fontSize = "16px";
-        labelElem.htmlFor = inputElemId;
-        labelElem.innerText = fmtVal(initialVal) + unitTxt;
+        // @ts-ignore
+        const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
+
+        const fmtVal = (v: unknown) => String(v).trim();
+        const toggleLabelText = (toggled: boolean) => toggled ? "On" : "Off";
+
+        let labelElem: HTMLLabelElement | undefined;
+        if(type === "slider") {
+          labelElem = document.createElement("label");
+          labelElem.classList.add("bytm-ftconf-label");
+          labelElem.style.marginRight = "10px";
+          labelElem.style.fontSize = "16px";
+          labelElem.htmlFor = inputElemId;
+          labelElem.innerText = fmtVal(initialVal) + unitTxt;
+
+          inputElem.addEventListener("input", () => {
+            if(labelElem)
+              labelElem.innerText = fmtVal(parseInt(inputElem.value)) + unitTxt;
+          });
+        }
+        else if(type === "toggle") {
+          labelElem = document.createElement("label");
+          labelElem.classList.add("bytm-ftconf-label");
+          labelElem.style.paddingLeft = "10px";
+          labelElem.style.paddingRight = "5px";
+          labelElem.style.fontSize = "16px";
+          labelElem.htmlFor = inputElemId;
+          labelElem.innerText = toggleLabelText(Boolean(initialVal)) + unitTxt;
+
+          inputElem.addEventListener("input", () => {
+            if(labelElem)
+              labelElem.innerText = toggleLabelText(inputElem.checked) + unitTxt;
+          });
+        }
 
         inputElem.addEventListener("input", () => {
-          if(labelElem)
-            labelElem.innerText = fmtVal(parseInt(inputElem.value)) + unitTxt;
+          let v = Number(String(inputElem.value).trim());
+          if(isNaN(v))
+            v = Number(inputElem.value);
+          if(typeof initialVal !== "undefined")
+            confChanged(featKey as keyof FeatureConfig, initialVal, (type !== "toggle" ? v : inputElem.checked));
         });
-      }
-      else if(type === "toggle") {
-        labelElem = document.createElement("label");
-        labelElem.classList.add("bytm-ftconf-label");
-        labelElem.style.paddingLeft = "10px";
-        labelElem.style.paddingRight = "5px";
-        labelElem.style.fontSize = "16px";
-        labelElem.htmlFor = inputElemId;
-        labelElem.innerText = toggleLabelText(Boolean(initialVal)) + unitTxt;
 
-        inputElem.addEventListener("input", () => {
-          if(labelElem)
-            labelElem.innerText = toggleLabelText(inputElem.checked) + unitTxt;
-        });
+        labelElem && ctrlElem.appendChild(labelElem);
+        ctrlElem.appendChild(inputElem);
+
+        ftConfElem.appendChild(ctrlElem);
       }
 
-      inputElem.addEventListener("input", () => {
-        let v = Number(String(inputElem.value).trim());
-        if(isNaN(v))
-          v = Number(inputElem.value);
-        if(typeof initialVal !== "undefined")
-          confChanged(key as keyof FeatureConfig, initialVal, (type !== "toggle" ? v : inputElem.checked));
-      });
+      featuresCont.appendChild(ftConfElem);
+    }
+  }
 
-      labelElem && ctrlElem.appendChild(labelElem);
-      ctrlElem.appendChild(inputElem);
+  //#SECTION scroll indicator
+  const scrollIndicator = document.createElement("img");
+  scrollIndicator.id = "bytm-menu-scroll-indicator";
+  scrollIndicator.role = "button";
+  scrollIndicator.title = "Click to scroll to the bottom";
+  scrollIndicator.src = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjQ4IiB2aWV3Qm94PSIwIC05NjAgOTYwIDk2MCIgd2lkdGg9IjQ4Ij4KICA8cGF0aCBmaWxsPSIjZmZmZmZmIiBkPSJNNDU3LjMwOC03NzkuOTk5djUxMy42OTJMMjEyLjAwMS01MTEuOTk5bC0zMiAzMS45OTlMNDgwLTE4MC4wMDEgNzc5Ljk5OS00ODBsLTMyLTMxLjk5OS0yNDUuMzA3IDI0NS42OTJ2LTUxMy42OTJoLTQ1LjM4NFoiLz4KPC9zdmc+";
+  //#DEBUG scrollIndicatorElem.src = await getResourceUrl("arrow_down");
+  featuresCont.appendChild(scrollIndicator);
+
+  scrollIndicator.addEventListener("click", () => {
+    const bottomAnchor = document.querySelector("#bytm-menu-bottom-anchor");
+    bottomAnchor?.scrollIntoView({
+      behavior: "smooth",
+    });
+  });
 
-      ftConfElem.appendChild(ctrlElem);
+  featuresCont.addEventListener("scroll", async (evt: Event) => {
+    const scrollPos = (evt.target as HTMLDivElement)?.scrollTop ?? 0;
+    const scrollIndicator = document.querySelector<HTMLImageElement>("#bytm-menu-scroll-indicator");
+
+    if(!scrollIndicator)
+      return;
+
+    if(scrollPos > 10 && scrollIndicatorShown) {
+      scrollIndicatorShown = false;
+      scrollIndicator.classList.add("hidden");
+      await pauseFor(200);
+      scrollIndicator.style.visibility = "hidden";
     }
+    else if(scrollPos <= 10 && !scrollIndicatorShown) {
+      scrollIndicatorShown = true;
+      scrollIndicator.style.visibility = "initial";
+      scrollIndicator.classList.remove("hidden");
+    }
+  });
 
-    featuresCont.appendChild(ftConfElem);
-  }
+  const bottomAnchor = document.createElement("div");
+  bottomAnchor.id = "bytm-menu-bottom-anchor";
+  featuresCont.appendChild(bottomAnchor);
 
   //#SECTION footer
   const footerCont = document.createElement("div");
   footerCont.id = "bytm-menu-footer-cont";
-  footerCont.style.display = "flex";
-  footerCont.style.flexDirection = "row";
-  footerCont.style.justifyContent = "space-between";
-  footerCont.style.padding = "20px 20px 10px 20px";
-  footerCont.style.marginTop = "10px";
-  footerCont.style.position = "sticky";
-  footerCont.style.bottom = "0";
 
   const footerElem = document.createElement("div");
   footerElem.id = "bytm-menu-footer";
@@ -287,8 +337,7 @@ export async function addMenu() {
   versionCont.style.display = "flex";
   versionCont.style.justifyContent = "space-around";
   versionCont.style.fontSize = "1.15em";
-  versionCont.style.marginTop = "10px";
-  versionCont.style.marginBottom = "5px";
+  versionCont.style.marginTop = "5px";
 
   const versionElem = document.createElement("span");
   versionElem.id = "bytm-menu-version";
@@ -304,7 +353,7 @@ export async function addMenu() {
 
   document.body.appendChild(backgroundElem);
 
-  log("Added menu element:", backgroundElem);
+  log("Added menu element");
 }
 
 //#MARKER utilities
@@ -315,7 +364,7 @@ export function closeMenu(e?: MouseEvent | KeyboardEvent) {
   isMenuOpen = false;
   e?.bubbles && e.stopPropagation();
 
-  document.body.classList.remove("bytm-disable-scroll");
+  document.body.removeAttribute("no-y-overflow");
   const menuBg = document.querySelector("#bytm-menu-bg") as HTMLElement;
 
   menuBg.style.visibility = "hidden";
@@ -328,9 +377,19 @@ export function openMenu() {
     return;
   isMenuOpen = true;
 
-  document.body.classList.add("bytm-disable-scroll");
+  document.body.setAttribute("no-y-overflow", "");
   const menuBg = document.querySelector("#bytm-menu-bg") as HTMLElement;
 
   menuBg.style.visibility = "visible";
   menuBg.style.display = "block";
+
+  const featuresCont = document.querySelector<HTMLElement>("#bytm-menu-opts");
+  const scrollIndicator = document.querySelector<HTMLElement>("#bytm-menu-scroll-indicator");
+
+  // disable scroll indicator if container doesn't scroll
+  if(featuresCont && scrollIndicator && !isScrollable(featuresCont).vertical) {
+    scrollIndicatorShown = false;
+    scrollIndicator.classList.add("hidden");
+    scrollIndicator.style.visibility = "hidden";
+  }
 }

+ 0 - 3
src/types.ts

@@ -7,9 +7,6 @@ export type LogLevel = 0 | 1;
 /** Which domain this script is currently running on */
 export type Domain = "yt" | "ytm";
 
-/** Feature category to be used in the menu for grouping features */
-export type FeatureCategory = "input" | "layout" | "lyrics";
-
 /** Feature configuration */
 export interface FeatureConfig {
   //#SECTION input

+ 9 - 0
src/utils.ts

@@ -225,3 +225,12 @@ export function getDomain(): Domain {
 export function getResourceUrl(name: keyof typeof resources) {
   return GM.getResourceUrl(name);
 }
+
+/** Checks if an element is scrollable in the horizontal and vertical directions */
+export function isScrollable(element: HTMLElement) {
+  const { overflowX, overflowY } = getComputedStyle(element);
+  return {
+    vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
+    horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth,
+  };
+}