Ver código fonte

feat: skip to time with number keys

Sv443 1 ano atrás
pai
commit
1328a4e558
6 arquivos alterados com 176 adições e 69 exclusões
  1. 11 2
      src/config.ts
  2. 10 4
      src/features/index.ts
  3. 148 60
      src/features/input.ts
  4. 4 2
      src/features/layout.ts
  5. 1 1
      src/features/menu/menu_old.ts
  6. 2 0
      src/types.ts

+ 11 - 2
src/config.ts

@@ -21,6 +21,7 @@ const migrations: ConfigMigrationsDict = {
   3: (oldData: Record<string, unknown>) => ({
     ...oldData,
     removeShareTrackingParam: true,
+    numKeysSkipToTime: true,
   }),
 };
 
@@ -39,10 +40,12 @@ const cfgMgr = new ConfigManager({
 
 /** Initializes the ConfigManager instance and loads persistent data into memory */
 export async function initConfig() {
-  const oldFmtVer = await GM.getValue(`_uucfgver-${cfgMgr.id}`, -1);
+  const oldFmtVer = Number(await GM.getValue(`_uucfgver-${cfgMgr.id}`, NaN));
   const data = await cfgMgr.loadData();
   log(`Initialized ConfigManager (format version = ${cfgMgr.formatVersion})`);
-  if(oldFmtVer !== cfgMgr.formatVersion)
+  if(isNaN(oldFmtVer))
+    info("Config data initialized with default values");
+  else if(oldFmtVer !== cfgMgr.formatVersion)
     info(`Config data migrated from version ${oldFmtVer} to ${cfgMgr.formatVersion}`);
   return data;
 }
@@ -63,3 +66,9 @@ export async function setDefaultFeatures() {
   await cfgMgr.saveDefaultData();
   info("Reset feature config to its default values");
 }
+
+/** Clears the feature config from the persistent storage */
+export async function clearConfig() {
+  await cfgMgr.deleteConfig();
+  info("Deleted config from persistent storage");
+}

+ 10 - 4
src/features/index.ts

@@ -19,7 +19,7 @@ export const categoryNames: Record<FeatureCategory, string> = {
   lyrics: "Lyrics",
 } as const;
 
-/** Contains all possible features with their default values and other config */
+/** Contains all possible features with their default values and other configuration */
 export const featInfo = {
   //#SECTION layout
   removeUpgradeTab: {
@@ -96,6 +96,7 @@ export const featInfo = {
     default: true,
   },
   switchSitesHotkey: {
+    hidden: true,
     desc: "TODO(v1.1): Which hotkey needs to be pressed to switch sites?",
     type: "hotkey",
     category: "input",
@@ -105,10 +106,9 @@ export const featInfo = {
       ctrl: false,
       meta: false,
     },
-    hidden: true,
   },
   disableBeforeUnloadPopup: {
-    desc: "Disable the confirmation popup that sometimes appears when trying to leave the site",
+    desc: "Prevent the confirmation popup that appears when trying to leave the site while a song is playing",
     type: "toggle",
     category: "input",
     default: false,
@@ -119,6 +119,12 @@ export const featInfo = {
     category: "input",
     default: true,
   },
+  numKeysSkipToTime: {
+    desc: "Enable skipping to a specific time in the video by pressing a number key (0-9)",
+    type: "toggle",
+    category: "input",
+    default: true,
+  },
 
   //#SECTION lyrics
   geniusLyrics: {
@@ -132,5 +138,5 @@ export const featInfo = {
     type: "toggle",
     category: "lyrics",
     default: true,
-  }
+  },
 } as const;

+ 148 - 60
src/features/input.ts

@@ -1,79 +1,82 @@
 import { getUnsafeWindow } from "@sv443-network/userutils";
 import { error, getVideoTime, info, log, warn } from "../utils";
 import type { Domain } from "../types";
+import { isMenuOpen } from "./menu/menu_old";
 
 //#MARKER arrow key skip
 
 export function initArrowKeySkip() {
-  document.addEventListener("keydown", onKeyDown);
-  log("Added key press listener");
+  document.addEventListener("keydown", (evt) => {
+    if(!["ArrowLeft", "ArrowRight"].includes(evt.code))
+      return;
+    // discard the event when a (text) input is currently active, like when editing a playlist
+    if(["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement?.tagName ?? "_"))
+      return info(`Captured valid key to skip forward or backward but the current active element is <${document.activeElement?.tagName.toLowerCase()}>, so the keypress is ignored`);
+
+    onArrowKeyPress(evt);
+  });
+  log("Added arrow key press listener");
 }
 
 /** Called when the user presses any key, anywhere */
-function onKeyDown(evt: KeyboardEvent) {
-  if(["ArrowLeft", "ArrowRight"].includes(evt.code)) {
-    // discard the event when a (text) input is currently active, like when editing a playlist
-    if(["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement?.tagName ?? "_"))
-      return info(`Captured valid key but the current active element is <${document.activeElement!.tagName.toLowerCase()}>, so the keypress is ignored`);
-
-    log(`Captured key '${evt.code}' in proxy listener`);
-
-    // ripped this stuff from the console, most of these are probably unnecessary but this was finnicky af and I am sick and tired of trial and error
-    const defaultProps = {
-      altKey: false,
-      ctrlKey: false,
-      metaKey: false,
-      shiftKey: false,
-      target: document.body,
-      currentTarget: document.body,
-      originalTarget: document.body,
-      explicitOriginalTarget: document.body,
-      srcElement: document.body,
-      type: "keydown",
-      bubbles: true,
-      cancelBubble: false,
-      cancelable: true,
-      isTrusted: true,
-      repeat: false,
-      // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
-      view: getUnsafeWindow(),
-    };
+function onArrowKeyPress(evt: KeyboardEvent) {
+  log(`Captured key '${evt.code}' in proxy listener`);
 
-    let invalidKey = false;
-    let keyProps = {};
+  // ripped this stuff from the console, most of these are probably unnecessary but this was finnicky af and I am sick and tired of trial and error
+  const defaultProps = {
+    altKey: false,
+    ctrlKey: false,
+    metaKey: false,
+    shiftKey: false,
+    target: document.body,
+    currentTarget: document.body,
+    originalTarget: document.body,
+    explicitOriginalTarget: document.body,
+    srcElement: document.body,
+    type: "keydown",
+    bubbles: true,
+    cancelBubble: false,
+    cancelable: true,
+    isTrusted: true,
+    repeat: false,
+    // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
+    view: getUnsafeWindow(),
+  };
 
-    switch(evt.code) {
-    case "ArrowLeft":
-      keyProps = {
-        code: "KeyH",
-        key: "h",
-        keyCode: 72,
-        which: 72,
-      };
-      break;
-    case "ArrowRight":
-      keyProps = {
-        code: "KeyL",
-        key: "l",
-        keyCode: 76,
-        which: 76,
-      };
-      break;
-    default:
-      invalidKey = true;
-      break;
-    }
+  let invalidKey = false;
+  let keyProps = {};
+
+  switch(evt.code) {
+  case "ArrowLeft":
+    keyProps = {
+      code: "KeyH",
+      key: "h",
+      keyCode: 72,
+      which: 72,
+    };
+    break;
+  case "ArrowRight":
+    keyProps = {
+      code: "KeyL",
+      key: "l",
+      keyCode: 76,
+      which: 76,
+    };
+    break;
+  default:
+    invalidKey = true;
+    break;
+  }
 
-    if(!invalidKey) {
-      const proxyProps = { code: "", ...defaultProps, ...keyProps };
+  if(!invalidKey) {
+    const proxyProps = { code: "", ...defaultProps, ...keyProps };
 
-      document.body.dispatchEvent(new KeyboardEvent("keydown", proxyProps));
+    document.body.dispatchEvent(new KeyboardEvent("keydown", proxyProps));
 
-      log(`Dispatched proxy keydown event: [${evt.code}] -> [${proxyProps.code}]`);
-    }
-    else
-      warn(`Captured key '${evt.code}' has no defined behavior`);
+    log(`Dispatched proxy keydown event: [${evt.code}] -> [${proxyProps.code}]`);
   }
+  else
+    warn(`Captured key '${evt.code}' has no defined behavior`);
 }
 
 //#MARKER site switch
@@ -173,3 +176,88 @@ export function initBeforeUnloadHook() {
     // @ts-ignore
   })(window.__proto__.addEventListener);
 }
+
+//#MARKER number keys skip to time
+
+/** Adds the ability to skip to a certain time in the video by pressing a number key (0-9) */
+export function initNumKeysSkip() {
+  document.addEventListener("keydown", (e) => {
+    if(!e.key.trim().match(/^[0-9]$/))
+      return;
+    if(isMenuOpen)
+      return;
+    // discard the event when a (text) input is currently active, like when editing a playlist or when the search bar is focused
+    if(
+      document.activeElement !== document.body
+      && !["progress-bar"].includes(document.activeElement?.id ?? "_")
+      && !["BUTTON", "A"].includes(document.activeElement?.tagName ?? "_")
+    )
+      return info("Captured valid key to skip video to but an unexpected element is focused, so the keypress is ignored");
+
+    skipToTimeKey(Number(e.key));
+  });
+  log("Added number key press listener");
+}
+
+/** Emulates a click on the video progress bar at the position calculated from the passed time key (0-9) */
+function skipToTimeKey(key: number) {
+  const getX = (timeKey: number, maxWidth: number) => {
+    if(timeKey >= 10)
+      return maxWidth;
+    return Math.floor((maxWidth / 10) * timeKey);
+  };
+
+  /** Calculate offsets of the bounding client rect of the passed element - see https://stackoverflow.com/a/442474/11187044 */
+  const getOffsetRect = (elem: HTMLElement) => {
+    let left = 0;
+    let top = 0;
+    const rect = elem.getBoundingClientRect();
+    while(elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) {
+      left += elem.offsetLeft - elem.scrollLeft;
+      top += elem.offsetTop - elem.scrollTop;
+      elem = elem.offsetParent as HTMLElement;
+    }
+    return {
+      top,
+      left,
+      width: rect.width,
+      height: rect.height,
+    };
+  };
+
+  // not technically a progress element but behaves pretty much the same
+  const progressElem = document.querySelector<HTMLProgressElement>("tp-yt-paper-slider#progress-bar tp-yt-paper-progress#sliderBar");
+  if(!progressElem)
+    return;
+
+  const rect = getOffsetRect(progressElem);
+
+  const x = getX(key, rect.width);
+  const y = rect.top - rect.height / 2;
+
+  log(`Skipping to time key ${key} (x offset: ${x}px of ${rect.width}px)`);
+
+  const evt = new MouseEvent("mousedown", {
+    clientX: x,
+    clientY: y,
+    // @ts-ignore
+    layerX: x,
+    layerY: rect.height / 2,
+    target: progressElem,
+    bubbles: true,
+    shiftKey: false,
+    ctrlKey: false,
+    altKey: false,
+    metaKey: false,
+    button: 0,
+    buttons: 1,
+    which: 1,
+    isTrusted: true,
+    offsetX: 0,
+    offsetY: 0,
+    // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
+    view: getUnsafeWindow(),
+  });
+
+  progressElem.dispatchEvent(evt);
+}

+ 4 - 2
src/features/layout.ts

@@ -114,7 +114,7 @@ function exchangeLogo() {
   });
 }
 
-/** Called whenever the menu exists to add a BYTM-Configuration button */
+/** Called whenever the menu exists to add a BYTM-Configuration button to the user menu popover */
 export async function addConfigMenuOption(container: HTMLElement) {
   const cfgOptElem = document.createElement("div");
   cfgOptElem.role = "button";
@@ -449,12 +449,14 @@ async function addQueueButtons(queueItem: HTMLElement) {
   queueItem.classList.add("bytm-has-queue-btns");
 }
 
-//#MARKER better clickable stuff
+//#MARKER anchor improvements
+
 
 // TODO: add to thumbnails in "songs" list on channel pages (/channel/$id)
 // TODO: add to thumbnails in playlists (/playlist?list=$id)
 // TODO:FIXME: only works for the first 7 items of each carousel shelf -> probably needs own mutation observer
 
+
 /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
 export function addAnchorImprovements() {
   //#SECTION carousel shelves

+ 1 - 1
src/features/menu/menu_old.ts

@@ -8,7 +8,7 @@ import "./menu_old.css";
 
 //#MARKER create menu elements
 
-let isMenuOpen = false;
+export let isMenuOpen = false;
 
 /** Threshold in pixels from the top of the options container that dictates for how long the scroll indicator is shown */
 const scrollIndicatorOffsetThreshold = 30;

+ 2 - 0
src/types.ts

@@ -43,6 +43,8 @@ export interface FeatureConfig {
   closeToastsTimeout: number;
   /** Remove the "si" tracking parameter from links in the share popup */
   removeShareTrackingParam: boolean;
+  /** Enable skipping to a specific time in the video by pressing a number key (0-9) */
+  numKeysSkipToTime: boolean;
 
   //#SECTION lyrics
   /** Add a button to the media controls to open the current song's lyrics on genius.com in a new tab */