e621 Enhanced Navigation & Shortcuts

Enhances e621 with keyboard shortcuts for voting, favoriting, and navigation. Features auto-skip for blacklisted content, improved pool navigation, and smart behavior that disables shortcuts when typing or using video player.

// ==UserScript==
// @name         e621 Enhanced Navigation & Shortcuts
// @namespace    https://github.com/Webscratcher/UserScripts
// @version      1.3.6
// @license      GPL3
// @description  Enhances e621 with keyboard shortcuts for voting, favoriting, and navigation. Features auto-skip for blacklisted content, improved pool navigation, and smart behavior that disables shortcuts when typing or using video player.
// @author       Webscratcher
// @match        https://e621.net/posts*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=e621.net
// @grant        unsafeWindow
// ==/UserScript==

// Configuration
const CONFIG = {
  shortcuts: {
    UPVOTE: { key: "KeyU", requiresShift: false },
    DOWNVOTE: { key: "KeyU", requiresShift: true },
    LIKE_AND_FAVORITE: { key: "KeyL", requiresShift: false },
    FAVORITE_AND_CONTINUE: { key: "KeyL", requiresShift: true },
    TOGGLE_FAVORITES: { key: "KeyI", requiresShift: false },
    TOGGLE_BLACKLIST: { key: "KeyB", requiresShift: false },
    NEXT_POST: { key: "ArrowRight", requiresShift: false },
    PREV_POST: { key: "ArrowLeft", requiresShift: false },
    TOGGLE_AUTOSKIP: { key: "KeyP", requiresShift: false },
  },
  selectors: {
    voteButtons: "div.image-vote-buttons",
    upvoteButton: "div.image-vote-buttons > a.post-vote-up-link > span",
    downvoteButton: "div.image-vote-buttons > a.post-vote-down-link > span",
    favoriteButton: "button#add-fav-button",
    unvaforiteButton: "button#remove-fav-button",
    imageViewer: "img#image",
    navigation: {
      next: "div#nav-links-top > div > ul > li > a.nav-link.next",
      prev: "div#nav-links-top > div > ul > li > a.nav-link.prev",
      searchField: "textarea#tags",
    },
    blacklist: {
      box: "section#blacklist-box > a",
      collapse: "#blacklist-collapse",
      blacklistedContent: "#image-container.blacklisted",
    },
    pool: {
      nav: "#nav-links-top",
      link: "div.pool-nav > ul > li > span > a",
    },
  },
  delays: {
    defaultSleep: 2,
    voteDelay: 1,
    autoSkipDelay: 3, // 3 seconds countdown before skipping
  },
};

// Utility functions
class Utils {
  static async sleep(seconds) {
    return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }

  static isElementVisible(element) {
    if (!element) return false;

    const style = window.getComputedStyle(element);
    const rect = element.getBoundingClientRect();

    return !(
      style.display === "none" ||
      style.visibility === "hidden" ||
      parseFloat(style.opacity) < 0.01 ||
      (rect.width === 0 && rect.height === 0)
    );
  }

  static isInteractiveElementFocused() {
    const activeElement = document.activeElement;
    if (!activeElement) return false;

    const interactiveElements = ["INPUT", "TEXTAREA", "SELECT", "VIDEO"];
    return (
      (interactiveElements.includes(activeElement.tagName) ||
        activeElement.isContentEditable) &&
      Utils.isElementVisible(activeElement)
    );
  }

  static async copyToClipboard(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (error) {
      console.error("Failed to copy to clipboard:", error);
      return false;
    }
  }
}

// DOM Manager for caching and managing DOM elements
class DOMManager {
  constructor() {
    this.elements = new Map();
    this.initializeElements();
  }

  initializeElements() {
    Object.entries(CONFIG.selectors).forEach(([key, selector]) => {
      if (typeof selector === "string") {
        this.elements.set(key, document.querySelector(selector));
      }
    });
  }

  getElement(key) {
    if (!this.elements.has(key) && key.split(".").length == 2) {
      let selector = CONFIG.selectors[key.split(".")[0]];
      const secondKey = key.split(".")[1];
      if (selector && selector[secondKey] !== null) {
        selector = selector[secondKey];
        this.elements.set(key, document.querySelector(selector));
      }
    }
    return this.elements.get(key);
  }

  getMetaData(key) {
    const metaTag = document.querySelector(`meta[name="${key}"]`);
    if (!metaTag) return null;

    return metaTag.getAttribute("content");
  }
}

// Vote Manager
class VoteManager {
  constructor(domManager) {
    this.dom = domManager;
  }

  isAlreadyUpvoted() {
    const upvoteButton = this.dom.getElement("upvoteButton");

    return upvoteButton.classList.contains("score-positive");
  }

  isAlreadyDownvoted() {
    const downvoteButton = this.dom.getElement("downvoteButton");

    return downvoteButton.classList.contains("score-negative");
  }

  isAlreadyFavorited() {
    const favoriteButton = this.dom.getElement("favoriteButton");
    return !Utils.isElementVisible(favoriteButton);
  }

  async upvote() {
    if (!this.isAlreadyUpvoted()) {
      const upvoteButton = this.dom
        .getElement("voteButtons")
        ?.querySelector(CONFIG.selectors.upvoteButton);
      upvoteButton?.click();
      await Utils.sleep(CONFIG.delays.voteDelay);
      return;
    }
    return;
  }

  async downvote() {
    if (!this.isAlreadyDownvoted()) {
      const downvoteButton = this.dom
        .getElement("voteButtons")
        ?.querySelector(CONFIG.selectors.downvoteButton);
      downvoteButton?.click();
      await Utils.sleep(CONFIG.delays.voteDelay);
      return;
    }
    return;
  }

  async favorite(disableUnfavorite = false) {
    if (!this.isAlreadyFavorited()) {
      const favoriteButton = this.dom.getElement("favoriteButton");
      favoriteButton?.click();
      await Utils.sleep(CONFIG.delays.voteDelay);
      return;
    } else if (this.isAlreadyFavorited() && !disableUnfavorite) {
      const unfavoritedButton = this.dom.getElement("unfavoriteButton");
      unfavoritedButton?.click();
      await Utils.sleep(CONFIG.delays.voteDelay);
      return;
    }
    return;
  }
}

// Navigation Manager
class NavigationManager {
  constructor(domManager) {
    this.dom = domManager;
  }

  goToNext() {
    const nextButton = document.querySelector(CONFIG.selectors.navigation.next);
    nextButton?.click();
  }

  goToPrev() {
    const prevButton = document.querySelector(CONFIG.selectors.navigation.prev);
    prevButton?.click();
  }
}

class UserActions {
  constructor(domManager) {
    this.domManager = domManager;
    this.LOCAL_STORAGE_KEY = "myCachedUsername";
    this.EXPIRATION_MS = 24 * 60 * 60 * 1000; // 1 day
  }

  // 1. Try to read the username from the meta tag
  getUsernameFromMeta() {
    const metaData = this.domManager.getMetaData("current-user-name");
    if (!metaData) {
      return null;
    } else {
      return metaData;
    }
  }

  // 2. Store username in localStorage with an expiration
  storeUsername(username) {
    const data = {
      username,
      expires: Date.now() + this.EXPIRATION_MS,
    };
    localStorage.setItem(this.LOCAL_STORAGE_KEY, JSON.stringify(data));
  }

  // 3. Retrieve username from localStorage if it's not expired
  getStoredUsername() {
    const raw = localStorage.getItem(this.LOCAL_STORAGE_KEY);
    if (!raw) return null;

    const { username, expires } = JSON.parse(raw);
    if (Date.now() > expires) {
      localStorage.removeItem(this.LOCAL_STORAGE_KEY);
      return null;
    }
    return username;
  }

  // 4. Main function to obtain the username
  getUsername() {
    // First, check localStorage
    let username = this.getStoredUsername();
    if (username) return username;

    // Then, check the meta tag
    username = this.getUsernameFromMeta();
    if (!username) {
      // Fallback to asking the user
      username = prompt("Please enter your username:")?.trim() || "";
    }

    if (username) this.storeUsername(username);
    return username;
  }

  // 5. Check if "-fav:<username>" is present in the search field and remove it if found
  ignoreFavoritePosts(wasShiftHeld = false, wasCtrlHeld = false) {
    const searchField = document.querySelector(
      CONFIG.selectors.navigation.searchField,
    );
    if (!searchField) return;

    const username = this.getUsername(); // Assume this.getUsername() retrieves the username
    if (!username) return;

    const negativeFavTag = `-fav:${username}`;
    const positiveFavTag = `fav:${username}`;

    // Regex to match the exact negative tag: '-fav:<username>' preceded by start or space,
    // and followed by space or end of string
    const negativeRegex = new RegExp(`(^|\\s)${negativeFavTag}(?=$|\\s)`, "g");
    // Regex to match the exact positive tag: 'fav:<username>' preceded by start or space,
    // and followed by space or end of string
    const positiveRegex = new RegExp(`(^|\\s)${positiveFavTag}(?=$|\\s)`, "g");

    // Helper to remove all occurrences (positive or negative) from the input
    const removeAllFavTags = (val) => {
      return val
        .replace(negativeRegex, "") // remove any negative
        .replace(positiveRegex, "") // remove any positive
        .replace(/\s+/g, " ") // normalize multiple spaces
        .trim();
    };

    // Helper to remove only the negative tag
    const removeNegativeFav = (val) => {
      return val.replace(negativeRegex, "").replace(/\s+/g, " ").trim();
    };

    // Helper to remove only the positive tag
    const removePositiveFav = (val) => {
      return val.replace(positiveRegex, "").replace(/\s+/g, " ").trim();
    };

    let currentValue = searchField.value || "";

    if (wasCtrlHeld) {
      // 1) CTRL key held: remove ALL fav tags
      currentValue = removeAllFavTags(currentValue);
    } else if (wasShiftHeld) {
      // 2) SHIFT key held => toggle "fav:<username>"
      if (positiveRegex.test(currentValue)) {
        // Already has 'fav:<username>', so remove it
        currentValue = removePositiveFav(currentValue);
      } else {
        // Remove any '-fav:<username>' first, then prepend 'fav:<username>'
        currentValue = removeNegativeFav(currentValue);
        currentValue = currentValue
          ? `${positiveFavTag} ${currentValue}`.trim()
          : positiveFavTag;
      }
    } else {
      // 3) NO SHIFT => toggle "-fav:<username>"
      if (negativeRegex.test(currentValue)) {
        // Already has '-fav:<username>', so remove it
        currentValue = removeNegativeFav(currentValue);
      } else {
        // Remove any 'fav:<username>' first, then prepend '-fav:<username>'
        currentValue = removePositiveFav(currentValue);
        currentValue = currentValue
          ? `${negativeFavTag} ${currentValue}`.trim()
          : negativeFavTag;
      }
    }

    // Update the input field
    searchField.value = currentValue;

    // Finally, find a button in the same parent container and click it to trigger the new search
    const parent = searchField.parentElement;
    if (parent) {
      const button = parent.querySelector("button");
      if (button) button.click();
    }
  }
}

// Blacklist Manager
class BlacklistManager {
  constructor(domManager) {
    this.dom = domManager;
  }

  async openBlacklist() {
    const element = document.querySelector(CONFIG.selectors.blacklist.collapse);
    if (element?.classList.contains("hidden")) {
      element.click();
      await Utils.sleep(CONFIG.delays.defaultSleep);
    }
  }

  async toggleBlacklist() {
    const toggles = ["disable", "re-enable"];
    for (const toggle of toggles) {
      const selector = `${CONFIG.selectors.blacklist.box}${toggle}-all-blacklists`;
      const element = document.querySelector(selector);

      if (element && Utils.isElementVisible(element)) {
        element.click();
        await Utils.sleep(CONFIG.delays.defaultSleep);
        break;
      }
    }
  }

  isImageBlacklisted() {
    // Check for the blacklisted image placeholder
    return (
      document.querySelector(CONFIG.selectors.blacklist.blacklistedContent) !==
      null
    );
  }
}

// Pool Manager
class PoolManager {
  constructor(domManager) {
    this.dom = domManager;
  }

  addCopyPoolID() {
    const navBar = this.dom.getElement("pool.nav");
    const poolLink = navBar?.querySelector(CONFIG.selectors.pool.link);

    if (!poolLink) return;

    const poolId = poolLink.href.split("/").pop();
    const copyButton = this.createCopyButton(poolId);
    navBar.appendChild(copyButton);
  }

  createCopyButton(poolId) {
    const container = document.createElement("div");
    container.className = "pool-nav";
    container.style.textAlign = "center";

    const button = document.createElement("a");
    button.textContent = `Copy Pool-ID: ${poolId}`;
    button.style.cursor = "pointer";
    button.addEventListener("click", async () => {
      const success = await Utils.copyToClipboard(poolId);
      alert(success ? `Copied Pool ID: ${poolId}` : "Failed to copy pool ID");
    });

    container.appendChild(button);
    return container;
  }
}

// Auto-Skip Manager
class AutoSkipManager {
  constructor(domManager, navigationManager, blacklistManager) {
    this.dom = domManager;
    this.navigationManager = navigationManager;
    this.blacklistManager = blacklistManager;
    this.skipTimer = null;
    this.isPaused = false;
    this.progressBarElement = null;
    this.progressBarContainer = null;
    this.pauseIndicatorElement = null;
    this.skipTimeout = CONFIG.delays.autoSkipDelay * 1000;
    this.startTime = 0;
    this.enabled = true;
  }

  createProgressBar() {
    // Only create if we're on a blacklisted image
    if (!this.blacklistManager.isImageBlacklisted()) {
      return null;
    }

    const navBar = this.dom.getElement("pool.nav");
    if (!navBar) return null;

    // Create container for the progress bar
    const container = document.createElement("div");
    container.className = "pool-nav auto-skip-container";
    container.style.textAlign = "center";
    container.style.display = "block";
    container.style.marginTop = "10px";

    // Add title
    const title = document.createElement("div");
    title.textContent = "Auto-skipping blacklisted image";
    title.style.marginBottom = "5px";
    title.style.fontWeight = "bold";
    container.appendChild(title);

    // Create progress bar container
    const progressBarContainer = document.createElement("div");
    progressBarContainer.style.width = "100%";
    progressBarContainer.style.height = "20px";
    progressBarContainer.style.backgroundColor = "#444";
    progressBarContainer.style.borderRadius = "3px";
    progressBarContainer.style.overflow = "hidden";

    // Create the progress bar (starts full)
    this.progressBarElement = document.createElement("div");
    this.progressBarElement.style.height = "100%";
    this.progressBarElement.style.width = "100%";
    this.progressBarElement.style.backgroundColor = "#ff0000"; // Red progress bar
    this.progressBarElement.style.transition = "width 0.1s linear";
    this.progressBarElement.style.float = "right"; // Right-to-left effect

    progressBarContainer.appendChild(this.progressBarElement);
    container.appendChild(progressBarContainer);

    // Create pause indicator
    this.pauseIndicatorElement = document.createElement("div");
    this.pauseIndicatorElement.style.marginTop = "5px";
    this.pauseIndicatorElement.style.fontSize = "12px";
    this.pauseIndicatorElement.textContent = "Press P to pause";
    container.appendChild(this.pauseIndicatorElement);

    // Append container after the pool ID button
    navBar.appendChild(container);

    this.progressBarContainer = container;
    return container;
  }

  updateProgressBar(secondsRemaining) {
    if (this.progressBarElement) {
      // Convert remaining seconds to percentage (from 100% to 0%)
      const percentRemaining = (secondsRemaining / this.skipTimeout) * 100;
      this.progressBarElement.style.width = `${percentRemaining}%`;
    }
  }

  togglePause() {
    this.isPaused = !this.isPaused;

    if (this.pauseIndicatorElement) {
      this.pauseIndicatorElement.textContent = this.isPaused
        ? "Paused (Press P to resume)"
        : "Press P to pause";
    }
  }

  toggle() {
    this.enabled = !this.enabled;

    // If disabled while running, clear any active timer and remove progress bar
    if (!this.enabled && this.skipTimer) {
      clearInterval(this.skipTimer);
      this.skipTimer = null;

      if (this.progressBarContainer && this.progressBarContainer.parentNode) {
        this.progressBarContainer.parentNode.removeChild(
          this.progressBarContainer,
        );
        this.progressBarContainer = null;
      }
    } else if (this.enabled && this.blacklistManager.isImageBlacklisted()) {
      // If we're enabling and on a blacklisted image, start the skip
      this.startAutoSkipCountdown();
    }

    // Show a notification about the state change
    const status = this.enabled ? "enabled" : "disabled";
    this.showNotification(`Auto-skip ${status}`);
  }

  showNotification(message) {
    const notification = document.createElement("div");
    notification.textContent = message;
    notification.style.position = "fixed";
    notification.style.top = "10px";
    notification.style.left = "50%";
    notification.style.transform = "translateX(-50%)";
    notification.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
    notification.style.color = "white";
    notification.style.padding = "10px 20px";
    notification.style.borderRadius = "5px";
    notification.style.zIndex = "10000";

    document.body.appendChild(notification);

    // Remove notification after 2 seconds
    setTimeout(() => {
      document.body.removeChild(notification);
    }, 2000);
  }

  startAutoSkipCountdown() {
    // If auto-skip is disabled or already a timer running, don't start
    if (!this.enabled || this.skipTimer) return;

    // Only create and start timer if we're on a blacklisted image
    if (!this.blacklistManager.isImageBlacklisted()) return;

    // Make sure we have a progress bar
    if (!this.progressBarContainer) {
      this.createProgressBar();
    }

    // Reset state
    this.isPaused = false;
    if (this.pauseIndicatorElement) {
      this.pauseIndicatorElement.textContent = "Press P to pause";
    }

    this.startTime = Date.now();

    this.skipTimer = setInterval(() => {
      if (!this.isPaused) {
        const elapsed = Date.now() - this.startTime;
        const remainingTime = Math.max(this.skipTimeout - elapsed, 0);

        this.updateProgressBar(remainingTime);

        if (elapsed >= this.skipTimeout) {
          clearInterval(this.skipTimer);
          this.skipTimer = null;

          // Remove the progress bar container
          if (
            this.progressBarContainer &&
            this.progressBarContainer.parentNode
          ) {
            this.progressBarContainer.parentNode.removeChild(
              this.progressBarContainer,
            );
            this.progressBarContainer = null;
          }

          // Navigate to next post
          this.navigationManager.goToNext();
        }
      }
    }, 50); // Update every 50ms for smooth animation
  }

  checkAndStartSkip() {
    // Check if current image is blacklisted and start timer if needed
    if (this.blacklistManager.isImageBlacklisted() && this.enabled) {
      // Clean up any existing progress bar
      if (this.progressBarContainer && this.progressBarContainer.parentNode) {
        this.progressBarContainer.parentNode.removeChild(
          this.progressBarContainer,
        );
        this.progressBarContainer = null;
      }

      // Clear any existing timer
      if (this.skipTimer) {
        clearInterval(this.skipTimer);
        this.skipTimer = null;
      }

      // Start a fresh countdown
      this.startAutoSkipCountdown();
    } else if (
      !this.blacklistManager.isImageBlacklisted() &&
      this.progressBarContainer
    ) {
      // If we're not on a blacklisted image but have a progress bar, remove it
      if (this.progressBarContainer.parentNode) {
        this.progressBarContainer.parentNode.removeChild(
          this.progressBarContainer,
        );
        this.progressBarContainer = null;
      }

      // Clear any existing timer
      if (this.skipTimer) {
        clearInterval(this.skipTimer);
        this.skipTimer = null;
      }
    }
  }
}

// Shortcut Manager
class ShortcutManager {
  constructor(
    voteManager,
    navigationManager,
    blacklistManager,
    userActions,
    autoSkipManager,
  ) {
    this.voteManager = voteManager;
    this.navigationManager = navigationManager;
    this.blacklistManager = blacklistManager;
    this.userActions = userActions;
    this.autoSkipManager = autoSkipManager;
    this.initializeShortcuts();
  }

  initializeShortcuts() {
    document.addEventListener("keydown", this.handleKeydown.bind(this));
  }

  async handleKeydown(event) {
    // Handle shortcuts based on configuration
    const shortcuts = CONFIG.shortcuts;
    const { code, shiftKey, ctrlKey, altKey } = event;

    // Allow to switch pages even if an interactive element is focused
    if (altKey && code === shortcuts.NEXT_POST.key) {
      this.navigationManager.goToNext();
    } else if (altKey && code === shortcuts.PREV_POST.key) {
      this.navigationManager.goToPrev();
    }

    if (Utils.isInteractiveElementFocused()) return;

    // Always respect the toggle auto-skip key press
    if (code === shortcuts.TOGGLE_AUTOSKIP.key) {
      // If a skip timer is active, just toggle pause state
      if (this.autoSkipManager.skipTimer) {
        this.autoSkipManager.togglePause();
      } else {
        // Otherwise, toggle the auto-skip feature
        this.autoSkipManager.toggle();
      }
      return;
    }

    // Generic shortcuts
    if (!shiftKey && code === shortcuts.UPVOTE.key) {
      await this.voteManager.upvote();
    } else if (shiftKey && code === shortcuts.DOWNVOTE.key) {
      await this.voteManager.downvote();
    } else if (code === shortcuts.NEXT_POST.key) {
      this.navigationManager.goToNext();
    } else if (code === shortcuts.PREV_POST.key) {
      this.navigationManager.goToPrev();
    } else if (code === shortcuts.TOGGLE_BLACKLIST.key) {
      await this.blacklistManager.toggleBlacklist();
    } else if (code === shortcuts.TOGGLE_FAVORITES.key) {
      this.userActions.ignoreFavoritePosts(shiftKey, ctrlKey);
    } else if (!shiftKey && code === shortcuts.LIKE_AND_FAVORITE.key) {
      await this.voteManager.favorite();
      await this.voteManager.upvote();
    } else if (shiftKey && code === shortcuts.FAVORITE_AND_CONTINUE.key) {
      await this.voteManager.favorite();
      await this.voteManager.upvote();

      this.navigationManager.goToNext();
    }
  }
}

// Site function modifier
class SiteModifier {
  constructor(domManager) {
    this.dom = domManager;
  }

  poolLinkOpenInNewTab() {
    const navBar = this.dom.getElement("pool.nav");
    let poolLink = navBar?.querySelector(CONFIG.selectors.pool.link);

    if (!poolLink) return;

    poolLink.target = "_blank";
  }
}

// Main App
class App {
  constructor() {
    this.domManager = new DOMManager();
    this.voteManager = new VoteManager(this.domManager);
    this.navigationManager = new NavigationManager(this.domManager);
    this.blacklistManager = new BlacklistManager(this.domManager);
    this.poolManager = new PoolManager(this.domManager);
    this.userActions = new UserActions(this.domManager);
    this.autoSkipManager = new AutoSkipManager(
      this.domManager,
      this.navigationManager,
      this.blacklistManager,
    );
    this.shortcutManager = new ShortcutManager(
      this.voteManager,
      this.navigationManager,
      this.blacklistManager,
      this.userActions,
      this.autoSkipManager,
    );
    this.siteModifier = new SiteModifier(this.domManager);

    // Set up mutation observer to detect page content changes
    this.setupMutationObserver();
  }

  setupMutationObserver() {
    // Only observe the specific container that would indicate a blacklisted image
    const imageContainer = document.querySelector("#image-container");
    if (!imageContainer) return;

    // Create a focused mutation observer that only watches for blacklist class changes
    const observer = new MutationObserver((mutations) => {
      // Only check for actual class changes
      const hasClassChange = mutations.some(
        (mutation) =>
          mutation.type === "attributes" && mutation.attributeName === "class",
      );

      if (hasClassChange) {
        // Check if we're on a blacklisted image and should start the auto-skip
        this.autoSkipManager.checkAndStartSkip();
      }
    });

    // Only observe the specific element and only for class changes
    observer.observe(imageContainer, {
      attributes: true,
      attributeFilter: ["class"],
    });

    // Also observe for URL changes, which indicate navigation between posts
    let lastUrl = location.href;
    const urlObserver = setInterval(() => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        // Small delay to ensure the page has updated
        setTimeout(() => {
          this.autoSkipManager.checkAndStartSkip();
        }, 100);
      }
    }, 500);
  }

  async initialize() {
    await this.blacklistManager.openBlacklist();
    this.poolManager.addCopyPoolID();
    this.siteModifier.poolLinkOpenInNewTab();

    // Create the progress bar if not already created
    this.autoSkipManager.createProgressBar();

    // Check for blacklisted image on initial load
    this.autoSkipManager.checkAndStartSkip();
  }
}

// Initialize the application when the window loads
window.addEventListener("load", () => {
  const app = new App();
  app.initialize().catch(console.error);
  console.log("Extended Shortcuts with Auto-Skip loaded :)");
});