Explorar o código

feat: add proxy hotkey system & next/previous hotkeys

Sv443 hai 11 horas
pai
achega
0da65d19fb

+ 9 - 9
assets/translations/README.md

@@ -16,15 +16,15 @@ To submit or edit a translation, please follow [this guide](../../contributing.m
 ### Translation progress:
 |   | Locale | Translated keys | Based on |
 | :----: | ------ | --------------- | :------: |
-|  | [`en-US`](./en-US.json) | `357` (default locale) |  |
-| ✅ | [`de-DE`](./de-DE.json) | `357/357` (100%) | ─ |
-|  | [`en-GB`](./en-GB.json) | `357/357` (100%) | `en-US` |
-| ⚠ | [`es-ES`](./es-ES.json) | `342/357` (95.8%) | ─ |
-| ⚠ | [`fr-FR`](./fr-FR.json) | `342/357` (95.8%) | ─ |
-| ⚠ | [`hi-IN`](./hi-IN.json) | `342/357` (95.8%) | ─ |
-| ⚠ | [`ja-JP`](./ja-JP.json) | `342/357` (95.8%) | ─ |
-| ⚠ | [`pt-BR`](./pt-BR.json) | `342/357` (95.8%) | ─ |
-| ⚠ | [`zh-CN`](./zh-CN.json) | `342/357` (95.8%) | ─ |
+|  | [`en-US`](./en-US.json) | `361` (default locale) |  |
+| ✅ | [`de-DE`](./de-DE.json) | `361/361` (100%) | ─ |
+|  | [`en-GB`](./en-GB.json) | `361/361` (100%) | `en-US` |
+| ‼️ | [`es-ES`](./es-ES.json) | `342/361` (94.7%) | ─ |
+| ‼️ | [`fr-FR`](./fr-FR.json) | `342/361` (94.7%) | ─ |
+| ‼️ | [`hi-IN`](./hi-IN.json) | `342/361` (94.7%) | ─ |
+| ‼️ | [`ja-JP`](./ja-JP.json) | `342/361` (94.7%) | ─ |
+| ‼️ | [`pt-BR`](./pt-BR.json) | `342/361` (94.7%) | ─ |
+| ‼️ | [`zh-CN`](./zh-CN.json) | `342/361` (94.7%) | ─ |
 
 <sub>
 ✅ - Fully translated

+ 8 - 4
assets/translations/de-DE.json

@@ -271,7 +271,7 @@
   "feature_desc_thumbnailOverlayBehavior": "Wann das Videoelement automatisch durch sein Thumbnail in höchster Auflösung ersetzt werden soll",
   "feature_helptext_thumbnailOverlayBehavior": "Das Thumbnail wird über dem Video oder Song angezeigt.\nDies spart keine Bandbreite, da das Video immer noch im Hintergrund geladen und abgespielt wird!",
   "feature_desc_thumbnailOverlayToggleBtnShown": "Füge einen Knopf zu den Mediensteuerelementen hinzu, um das Thumbnail manuell zu aktivieren",
-  "feature_helptext_thumbnailOverlayToggleBtnShown": "Dieser Knopf ermöglicht es dir, das Thumbnail manuell ein- und auszuschalten. Dies wird nicht beeinflusst, wenn das Overlay auf \"nie gezeigt\" eingestellt ist.\nSobald ein neues Video oder Lied abgespielt wird, wird der Standardzustand wiederhergestellt.\nHalte Shift gedrückt, während du klickst oder drücke die mittlere Maustaste, um das Thumbnail in höchster Qualität in einem neuen Tab zu öffnen.",
+  "feature_helptext_thumbnailOverlayToggleBtnShown": "Dieser Knopf ermöglicht es dir, das Thumbnail manuell ein- und auszuschalten. Dies wird nicht beeinflusst, wenn das Overlay auf \"nie gezeigt\" eingestellt ist.\nSobald ein neues Video/Lied abgespielt wird, wird der Standardzustand wiederhergestellt.\nHalte Shift gedrückt, während du klickst oder drücke die mittlere Maustaste, um das Thumbnail in höchster Qualität in einem neuen Tab zu öffnen.",
   "feature_desc_thumbnailOverlayShowIndicator": "Zeige einen Indikator in der unteren rechten Ecke des Thumbnails, während es aktiv ist?",
   "feature_desc_thumbnailOverlayIndicatorOpacity": "Deckkraft des Thumbnail-Indikators",
   "feature_desc_thumbnailOverlayImageFit": "Wie das Thumbnail über dem Videoelement angezeigt werden soll",
@@ -339,9 +339,13 @@
   "feature_desc_switchBetweenSites": "Füge einen Hotkey hinzu, um zwischen den YT und YTM Seiten zu wechseln",
   "feature_helptext_switchBetweenSites": "Wenn du auf YouTube oder YouTube Music bist, kannst du mit diesem Hotkey zur anderen Seite wechseln, während du auf demselben Video / Song bleibst.",
   "feature_desc_switchSitesHotkey": "Der Hotkey, um zwischen den Seiten zu wechseln",
-  "feature_desc_likeDislikeHotkeys": "Füge Hotkeys hinzu, um das aktuell spielende Video oder Lied zu liken oder disliken",
-  "feature_desc_likeHotkey": "Der Hotkey, um das aktuell spielende Video oder Lied zu liken",
-  "feature_desc_dislikeHotkey": "Der Hotkey, um das aktuell spielende Video oder Lied zu disliken",
+  "feature_desc_likeDislikeHotkeys": "Füge Hotkeys hinzu, um das aktuell spielende Video/Lied zu liken oder disliken",
+  "feature_desc_likeHotkey": "Der Hotkey, um das aktuell spielende Video/Lied zu liken",
+  "feature_desc_dislikeHotkey": "Der Hotkey, um das aktuell spielende Video/Lied zu disliken",
+  "feature_desc_rebindNextAndPrevious": "Die Hotkeys neu belegen, die zum nächsten (J) oder vorherigen (K) Video/Lied springen",
+  "feature_desc_forceReboundNextAndPrevious": "Erlaube nur das Springen zum nächsten oder vorherigen Video/Lied mit den neuen Hotkeys",
+  "feature_desc_nextHotkey": "Der Hotkey, um zum nächsten Video/Lied zu springen",
+  "feature_desc_previousHotkey": "Der Hotkey, um zum vorherigen Video/Lied zu springen",
 
   "feature_desc_geniusLyrics": "Füge einen Knopf zu dem aktuell spielenden Song hinzu, um den Songtext auf genius.com zu öffnen",
   "feature_desc_errorOnLyricsNotFound": "Zeige einen Error, wenn die Songtext-Seite für den aktuell spielenden Song nicht gefunden werden konnte",

+ 13 - 9
assets/translations/en-US.json

@@ -269,9 +269,9 @@
   "feature_desc_fixSpacing": "Fix spacing issues in the layout",
   "feature_helptext_fixSpacing": "There are various locations in the user interface where the spacing between elements is inconsistent. This feature fixes those issues.",
   "feature_desc_thumbnailOverlayBehavior": "When to automatically replace the video element with its thumbnail in the highest resolution",
-  "feature_helptext_thumbnailOverlayBehavior": "The thumbnail will be shown over top of the currently playing video or song as an overlay.\nThis means you will not save any bandwidth as the video will still be loaded and played in the background!",
+  "feature_helptext_thumbnailOverlayBehavior": "The thumbnail will be shown over top of the currently playing video/song as an overlay.\nThis means you will not save any bandwidth as the video will still be loaded and played in the background!",
   "feature_desc_thumbnailOverlayToggleBtnShown": "Add a button to the media controls to manually toggle the thumbnail",
-  "feature_helptext_thumbnailOverlayToggleBtnShown": "This button will allow you to manually toggle the thumbnail on and off. This is not affected if the thumbnail replacement option is set to \"never\".\nOnce a new video or song starts playing, the default state will be restored.\nHold shift while clicking or press the middle mouse button to open the thumbnail of the highest quality in a new tab.",
+  "feature_helptext_thumbnailOverlayToggleBtnShown": "This button will allow you to manually toggle the thumbnail on and off. This is not affected if the thumbnail replacement option is set to \"never\".\nOnce a new video/song starts playing, the default state will be restored.\nHold shift while clicking or press the middle mouse button to open the thumbnail of the highest quality in a new tab.",
   "feature_desc_thumbnailOverlayShowIndicator": "Show an indicator in the bottom right corner of the thumbnail while it's active?",
   "feature_desc_thumbnailOverlayIndicatorOpacity": "Opacity of the thumbnail indicator",
   "feature_desc_thumbnailOverlayImageFit": "How to fit the thumbnail image over the video element",
@@ -320,8 +320,8 @@
   "feature_helptext_arrowKeySupport": "Normally you can only skip forwards and backwards by a fixed 10 second interval with the keys \"H\" and \"L\". This feature allows you to use the arrow keys to skip forwards and backwards and change the volume.\nTo change the amount of seconds to skip, use the option below.",
   "feature_desc_arrowKeySkipBy": "By how many seconds to skip when using the arrow keys",
   "feature_desc_arrowKeyVolumeStep": "How many percent to increase or decrease the volume by when using the arrow keys",
-  "feature_desc_frameSkip": "Use the period and comma keys to skip forwards and backwards by a frame when the video or song is paused",
-  "feature_desc_frameSkipWhilePlaying": "Also allow skipping by a frame while the video or song is still playing",
+  "feature_desc_frameSkip": "Use the period and comma keys to skip forwards and backwards by a frame when the video/song is paused",
+  "feature_desc_frameSkipWhilePlaying": "Also allow skipping by a frame while the video/song is still playing",
   "feature_desc_frameSkipAmount": "How many seconds to skip with the period and comma keys",
   "feature_helptext_frameSkipAmount": "This same feature which is natively available on YT skips by a fixed value of about 0.0417 seconds. If needed, adjust it to a different value here.",
   "feature_desc_anchorImprovements": "Add and improve links all over the page so things can be opened in a new tab easier",
@@ -336,12 +336,16 @@
   "feature_btn_autoLikeOpenMgmtDialog": "Open dialog",
   "feature_btn_autoLikeOpenMgmtDialog_running": "Opening...",
   
-  "feature_desc_switchBetweenSites": "Add a hotkey to switch between the YT and YTM sites on a video or song",
-  "feature_helptext_switchBetweenSites": "Pressing this hotkey will switch to the other site if you are on YouTube or YouTube Music while staying on the same video or song.",
+  "feature_desc_switchBetweenSites": "Add a hotkey to switch between the YT and YTM sites on a video/song",
+  "feature_helptext_switchBetweenSites": "Pressing this hotkey will switch to the other site if you are on YouTube or YouTube Music while staying on the same video/song.",
   "feature_desc_switchSitesHotkey": "The hotkey to switch between the sites",
-  "feature_desc_likeDislikeHotkeys": "Add hotkeys to like and dislike the currently playing video or song",
-  "feature_desc_likeHotkey": "The hotkey to like the current video or song",
-  "feature_desc_dislikeHotkey": "The hotkey to dislike the current video or song",
+  "feature_desc_likeDislikeHotkeys": "Add hotkeys to like and dislike the currently playing video/song",
+  "feature_desc_likeHotkey": "The hotkey to like the current video/song",
+  "feature_desc_dislikeHotkey": "The hotkey to dislike the current video/song",
+  "feature_desc_rebindNextAndPrevious": "Rebind the hotkeys to skip to the next (J) or previous (K) video/song",
+  "feature_desc_forceReboundNextAndPrevious": "Only allow skipping to the next or previous video/song with the new hotkeys",
+  "feature_desc_nextHotkey": "The hotkey to skip to the next video/song",
+  "feature_desc_previousHotkey": "The hotkey to skip to the previous video/song",
 
   "feature_desc_geniusLyrics": "Add a button to the media controls of the currently playing song to open its lyrics on genius.com",
   "feature_desc_errorOnLyricsNotFound": "Show an error when the lyrics page for the currently playing song couldn't be found",

+ 19 - 13
src/config.ts

@@ -136,17 +136,23 @@ export const migrations: DataMigrationsDict = {
     ]);
   },
   // 9 -> 10 (v2.3.0)
-  10: (oldData: FeatureConfig) => useNewDefaultIfUnchanged(
-    useDefaultConfig(oldData, [
-      "aboveQueueBtnsSticky", "autoScrollToActiveSongMode",
-      "frameSkip", "frameSkipWhilePlaying",
-      "frameSkipAmount", "watchPageFullSize",
-      "arrowKeyVolumeStep", "likeDislikeHotkeys",
-      "likeHotkey", "dislikeHotkey",
-    ]), [
-      { key: "lyricsCacheMaxSize", oldDefault: 2000 },
-    ],
-  ),
+  10: (oldData: FeatureConfig) => {
+    const migData = useNewDefaultIfUnchanged(
+      useDefaultConfig(oldData, [
+        "aboveQueueBtnsSticky", "autoScrollToActiveSongMode",
+        "frameSkip", "frameSkipWhilePlaying",
+        "frameSkipAmount", "watchPageFullSize",
+        "arrowKeyVolumeStep", "likeDislikeHotkeys",
+        "likeHotkey", "dislikeHotkey",
+        "rebindNextAndPrevious", "forceReboundNextAndPrevious",
+        "nextHotkey", "previousHotkey",
+      ]), [
+        { key: "lyricsCacheMaxSize", oldDefault: 2000 },
+      ],
+    );
+    migData.lyricsCacheMaxSize = clamp(migData.lyricsCacheMaxSize, featInfo.lyricsCacheMaxSize.min, featInfo.lyricsCacheMaxSize.max);
+    return migData;
+  },
 } as const satisfies DataMigrationsDict;
 
 /** Uses the default config as the base, then overwrites all values with the passed {@linkcode baseData}, then sets all passed {@linkcode resetKeys} to their default values */
@@ -242,8 +248,8 @@ export function getFeatures(): FeatureConfig {
 }
 
 /** Returns the value of the feature with the given key from the in-memory cache, as a copy */
-export function getFeature<TKey extends FeatureKey>(key: TKey): FeatureConfig[TKey] {
-  return configStore.getData()[key];
+export function getFeature<TKey extends FeatureKey>(key: TKey | "_"): FeatureConfig[TKey] {
+  return configStore.getData()[key as TKey];
 }
 
 /** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */

+ 96 - 10
src/features/hotkeys.ts

@@ -1,3 +1,4 @@
+import { getUnsafeWindow } from "@sv443-network/userutils";
 import { getFeature } from "../config.js";
 import { inputIgnoreTagNames } from "./input.js";
 import { siteEvents } from "../siteEvents.js";
@@ -5,21 +6,19 @@ import { enableDiscardBeforeUnload } from "./behavior.js";
 import { getLikeDislikeBtns, getVideoTime } from "../utils/dom.js";
 import { getDomain } from "../utils/misc.js";
 import { error, info, log, warn } from "../utils/logging.js";
-import type { Domain, HotkeyObj } from "../types.js";
+import type { Domain, FeatKeysOfType, HotkeyObj } from "../types.js";
 
 export async function initHotkeys() {
   const promises: Promise<void>[] = [];
 
-  if(getFeature("likeDislikeHotkeys"))
-    promises.push(initLikeDislikeHotkeys());
-
-  if(getFeature("switchBetweenSites"))
-    promises.push(initSiteSwitch());
+  promises.push(initLikeDislikeHotkeys());
+  promises.push(initSiteSwitch());
+  promises.push(initProxyHotkeys());
 
   return await Promise.allSettled(promises);
 }
 
-function keyPressed(e: KeyboardEvent, hk: HotkeyObj) {
+function hotkeyMatches(e: KeyboardEvent, hk: HotkeyObj) {
   return e.code === hk.code && e.shiftKey === hk.shift && e.ctrlKey === hk.ctrl && e.altKey === hk.alt;
 }
 
@@ -37,7 +36,7 @@ export async function initSiteSwitch() {
       return;
     if(inputIgnoreTagNames.includes(document.activeElement?.tagName ?? ""))
       return;
-    if(siteSwitchEnabled && keyPressed(e, getFeature("switchSitesHotkey")))
+    if(siteSwitchEnabled && hotkeyMatches(e, getFeature("switchSitesHotkey")))
       switchSite(domain === "yt" ? "ytm" : "yt");
   });
   siteEvents.on("hotkeyInputActive", (state) => {
@@ -104,9 +103,96 @@ async function initLikeDislikeHotkeys() {
 
     const { likeBtn, dislikeBtn } = getLikeDislikeBtns();
 
-    if(keyPressed(e, getFeature("likeHotkey")))
+    if(hotkeyMatches(e, getFeature("likeHotkey")))
       likeBtn?.click();
-    else if(keyPressed(e, getFeature("dislikeHotkey")))
+    else if(hotkeyMatches(e, getFeature("dislikeHotkey")))
       dislikeBtn?.click();
   });
 }
+
+//#region rebound hotkeys
+
+type HotkeyProxyGroup = {
+  /** The feature key that contains the hotkey object */
+  hkFeatKey: FeatKeysOfType<HotkeyObj>;
+  /** Called when the hotkey was pressed, regardless of the individual hotkey feature's enabled state */
+  onBeforePress?: (e: KeyboardEvent) => void | Promise<void>;
+  /** Called when the hotkey was pressed and the feature is toggled on */
+  onPress: (e: KeyboardEvent) => void | Promise<void>;
+};
+
+type ProxyHotkeys = Partial<Record<FeatKeysOfType<boolean>, HotkeyProxyGroup[]>>;
+
+let lastProxyHk = 0;
+
+/** Handles all proxy hotkeys, which trigger other hotkeys instead of their own actions */
+async function initProxyHotkeys() {
+  const suppressForceRebind = (e: KeyboardEvent) => {
+    if(["KeyJ", "KeyK"].includes(e.code) && getFeature("forceReboundNextAndPrevious")) {
+      e.preventDefault();
+      e.stopImmediatePropagation();
+    }
+  };
+
+  /** All proxy hotkey groups, identified by the feature key that toggles them off or on */
+  const proxyHotkeys: ProxyHotkeys = {
+    "rebindNextAndPrevious": [
+      {
+        hkFeatKey: "nextHotkey",
+        onBeforePress: suppressForceRebind,
+        onPress() {
+          document.body.dispatchEvent(new KeyboardEvent("keydown", {
+            code: "KeyJ",
+            key: "j",
+            keyCode: 74,
+            which: 74,
+            bubbles: true,
+            cancelable: false,
+            view: getUnsafeWindow(),
+          }));
+        },
+      },
+      {
+        hkFeatKey: "previousHotkey",
+        onBeforePress: suppressForceRebind,
+        onPress() {
+          document.body.dispatchEvent(new KeyboardEvent("keydown", {
+            code: "KeyK",
+            key: "k",
+            keyCode: 75,
+            which: 75,
+            bubbles: true,
+            cancelable: false,
+            view: getUnsafeWindow(),
+          }));
+        },
+      },
+    ],
+  } as const;
+
+  document.addEventListener("keydown", (e) => {
+    for(const [featKey, group] of Object.entries(proxyHotkeys)) {
+      if(!getFeature(featKey as "_"))
+        continue;
+
+      for(const { hkFeatKey, onPress, ...rest } of group) {
+        // prevent hotkeys from triggering each other:
+        if(Date.now() - lastProxyHk < 15) // (holding keys makes them repeat every ~30ms)
+          return;
+        lastProxyHk = Date.now();
+
+        if("onBeforePress" in rest)
+          rest.onBeforePress?.(e);
+
+        if(hotkeyMatches(e, getFeature(hkFeatKey))) {
+          !e.defaultPrevented && e.preventDefault();
+          e.bubbles && e.stopImmediatePropagation();
+          onPress(e);
+        }
+      }
+    }
+  }, {
+    // ensure precedence over YTM's own listeners:
+    capture: true,
+  });
+}

+ 48 - 1
src/features/index.ts

@@ -733,6 +733,53 @@ export const featInfo = {
     reloadRequired: false,
     enable: noop,
   },
+  rebindNextAndPrevious: {
+    type: "toggle",
+    category: "hotkeys",
+    supportedSites: ["ytm"],
+    default: false,
+    reloadRequired: false,
+    enable: noop,
+    textAdornment: adornments.ytmOnly,
+  },
+  forceReboundNextAndPrevious: {
+    type: "toggle",
+    category: "hotkeys",
+    supportedSites: ["ytm"],
+    default: true,
+    reloadRequired: false,
+    enable: noop,
+    advanced: true,
+    textAdornment: () => combineAdornments([adornments.ytmOnly, adornments.advanced]),
+  },
+  nextHotkey: {
+    type: "hotkey",
+    category: "hotkeys",
+    supportedSites: ["ytm"],
+    default: {
+      code: "KeyN",
+      shift: false,
+      ctrl: false,
+      alt: false,
+    },
+    reloadRequired: false,
+    enable: noop,
+    textAdornment: adornments.ytmOnly,
+  },
+  previousHotkey: {
+    type: "hotkey",
+    category: "hotkeys",
+    supportedSites: ["ytm"],
+    default: {
+      code: "KeyP",
+      shift: false,
+      ctrl: false,
+      alt: false,
+    },
+    reloadRequired: false,
+    enable: noop,
+    textAdornment: adornments.ytmOnly,
+  },
 
   //#region cat:lyrics
   geniusLyrics: {
@@ -780,7 +827,7 @@ export const featInfo = {
     supportedSites: ["ytm"],
     default: 5000,
     min: 1000,
-    max: 50_000,
+    max: 25_000,
     step: 500,
     unit: (val: number) => ` ${tp("unit_entries", val)}`,
     renderValue: renderNumberVal,

+ 2 - 1
src/index.ts

@@ -93,7 +93,6 @@ function preInit() {
     if(unsupportedHandlers.includes(GM?.info?.scriptHandler ?? "_"))
       return showPrompt({ type: "alert", message: `BetterYTM does not work when using ${GM.info.scriptHandler} as the userscript manager extension and will be disabled.\nI recommend using either ViolentMonkey, TamperMonkey or GreaseMonkey.`, denyBtnText: "Close" });
 
-    log("Session ID:", getSessionId());
     initInterface();
     setLogLevel(defaultLogLevel);
 
@@ -116,6 +115,8 @@ async function init() {
     const features = await initConfig();
     setLogLevel(features.logLevel);
 
+    info("Session ID:", getSessionId());
+
     await initLyricsCache();
 
     const initLoc = features.locale ?? "en-US";

+ 4 - 2
src/interface.ts

@@ -250,11 +250,13 @@ let pluginsInitialized = false;
 export function initPlugins() {
   emitInterface("bytm:registerPlugin", (def: PluginDef) => registerPlugin(def));
 
-  getUnsafeWindow().addEventListener("bytm:ready", () => {
+  window.addEventListener("bytm:ready", () => {
     pluginsInitialized = true;
     if(registeredPlugins.size > 0)
       log(`Registered ${registeredPlugins.size} ${autoPlural("plugin", registeredPlugins.size)}`);
-  }, { once: true });
+  }, {
+    once: true,
+  });
 }
 
 /** Registers a plugin on the BYTM interface. */

+ 23 - 5
src/types.ts

@@ -116,6 +116,13 @@ export type ColorLightnessPref = "darker" | "normal" | "lighter";
 
 export type LikeDislikeState = "LIKE" | "DISLIKE" | "INDIFFERENT";
 
+//#region utility
+
+/** Returns a union of all keys of {@linkcode T} whose values are of type {@linkcode U} */
+export type KeysOfType<T, U> = {
+  [K in keyof T]: T[K] extends U ? K : never
+}[keyof T];
+
 //#region global
 
 /** All properties of the `unsafeWindow.BYTM` object (also called "plugin interface") */
@@ -394,9 +401,12 @@ export type InterfaceFunctions = {
 
 //#region feature defs
 
-/** Feature identifier */
+/** Feature identifier key */
 export type FeatureKey = keyof FeatureConfig;
 
+/** Union of all feature identifier keys, where the value is of the specified type {@linkcode TType} */
+export type FeatKeysOfType<TType> = KeysOfType<FeatureConfig, TType>;
+
 /** Feature category identifier */
 export type FeatureCategory =
   | "layout"
@@ -625,16 +635,24 @@ export interface FeatureConfig {
   autoLikeOpenMgmtDialog: undefined;
 
   //#region hotkeys
-  /** Add a hotkey to switch between the YT and YTM sites on a video / song */
+  /** Add a hotkey to switch between the YT and YTM sites on a video/song */
   switchBetweenSites: boolean;
   /** The hotkey that needs to be pressed to initiate the site switch */
   switchSitesHotkey: HotkeyObj;
-  /** Add hotkeys for liking and disliking the current video / song */
+  /** Add hotkeys for liking and disliking the current video/song */
   likeDislikeHotkeys: boolean;
-  /** The hotkey that needs to be pressed to like the current video / song */
+  /** The hotkey that needs to be pressed to like the current video/song */
   likeHotkey: HotkeyObj;
-  /** The hotkey that needs to be pressed to dislike the current video / song */
+  /** The hotkey that needs to be pressed to dislike the current video/song */
   dislikeHotkey: HotkeyObj;
+  /** Whether to rebind the next [J] and previous [K] keys */
+  rebindNextAndPrevious: boolean;
+  /** Whether to only allow the new hotkeys to skip to the next or previous video/song */
+  forceReboundNextAndPrevious: boolean;
+  /** The hotkey that needs to be pressed to skip to the next video/song */
+  nextHotkey: HotkeyObj;
+  /** The hotkey that needs to be pressed to skip to the previous video/song */
+  previousHotkey: HotkeyObj;
 
   //#region lyrics
   /** Add a button to the media controls to open the current song's lyrics on genius.com in a new tab */