Password Revealer

Password Field Content Via - Reveal On Focus / Preview On Hover / Toggle On Double-Click / Always Visible | Switch Display Mode Via Menu Or Shortcut (Meta/Ctrl+Alt+P)

// ==UserScript==
// @name               Password Revealer
// @name:zh-CN         密码显示助手
// @name:zh-TW         密碼顯示助手
// @description        Password Field Content Via - Reveal On Focus / Preview On Hover / Toggle On Double-Click / Always Visible | Switch Display Mode Via Menu Or Shortcut (Meta/Ctrl+Alt+P)
// @description:zh-CN  密码输入框内容可聚焦即显 / 悬浮即览 / 双击切换 / 始终可见 | 通过菜单或快捷键(Meta/Ctrl+Alt+P)切换显示模式
// @description:zh-TW  密碼輸入框內容可聚焦即顯 / 懸停即覽 / 雙擊切換 / 始終可見 | 透過選單或快速鍵(Meta/Ctrl+Alt+P)切換顯示模式
// @version            1.5.0
// @icon               https://raw.githubusercontent.com/MiPoNianYou/UserScripts/main/Icons/Password-Revealer-Icon.svg
// @author             念柚
// @namespace          https://github.com/MiPoNianYou/UserScripts
// @supportURL         https://github.com/MiPoNianYou/UserScripts/issues
// @license            GPL-3.0
// @match              *://*/*
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// @grant              GM_unregisterMenuCommand
// @grant              GM_addStyle
// @run-at             document-idle
// ==/UserScript==

(function () {
  "use strict";

  const Config = {
    SCRIPT_SETTINGS: {
      UI_FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
      ANIMATION_DURATION_MS: 300,
      NOTIFICATION_VISIBILITY_DURATION_MS: 2000,
    },
    MODES: {
      FOCUS: "Focus",
      HOVER: "Hover",
      DBLCLICK: "DoubleClick",
      ALWAYS_SHOW: "AlwaysShow",
    },
    get VALID_MODES() {
      return [
        this.MODES.FOCUS,
        this.MODES.HOVER,
        this.MODES.DBLCLICK,
        this.MODES.ALWAYS_SHOW,
      ];
    },
    ELEMENT_IDS: {
      MODE_NOTIFICATION: "PasswordRevealerModeNotification",
    },
    CSS_CLASSES: {
      MODE_NOTIFICATION_VISIBLE: "pr-mode-notification--visible",
      BREATHING_DOT: "pr-breathing-dot",
      MODE_NOTIFICATION_MESSAGE: "pr-mode-notification-message",
    },
    ATTRIBUTES: {
      PROCESSED: "data-password-revealer-processed",
    },
    UI_TEXTS: {
      "zh-CN": {
        SCRIPT_TITLE: "密码显示助手",
        MENU_CMD_FOCUS: "「聚焦即显」模式",
        MENU_CMD_HOVER: "「悬浮即览」模式",
        MENU_CMD_DBLCLICK: "「双击切换」模式",
        MENU_CMD_ALWAYS_SHOW: "「始终可见」模式",
        ALERT_MSG_FOCUS: "模式已切换为「聚焦即显」",
        ALERT_MSG_HOVER: "模式已切换为「悬浮即览」",
        ALERT_MSG_DBLCLICK: "模式已切换为「双击切换」",
        ALERT_MSG_ALWAYS_SHOW: "模式已切换为「始终可见」",
      },
      "zh-TW": {
        SCRIPT_TITLE: "密碼顯示助手",
        MENU_CMD_FOCUS: "「聚焦即顯」模式",
        MENU_CMD_HOVER: "「懸停即覽」模式",
        MENU_CMD_DBLCLICK: "「雙擊切換」模式",
        MENU_CMD_ALWAYS_SHOW: "「始終可見」模式",
        ALERT_MSG_FOCUS: "模式已切換為「聚焦即顯」",
        ALERT_MSG_HOVER: "模式已切換為「懸停即覽」",
        ALERT_MSG_DBLCLICK: "模式已切換為「雙擊切換」",
        ALERT_MSG_ALWAYS_SHOW: "模式已切換為「始終可見」",
      },
      "en-US": {
        SCRIPT_TITLE: "Password Revealer",
        MENU_CMD_FOCUS: "「Reveal On Focus」Mode",
        MENU_CMD_HOVER: "「Preview On Hover」Mode",
        MENU_CMD_DBLCLICK: "「Toggle On Double-Click」Mode",
        MENU_CMD_ALWAYS_SHOW: "「Always Visible」Mode",
        ALERT_MSG_FOCUS: "Mode Switched To 「Reveal On Focus」",
        ALERT_MSG_HOVER: "Mode Switched To 「Preview On Hover」",
        ALERT_MSG_DBLCLICK: "Mode Switched To 「Toggle On Double-Click」",
        ALERT_MSG_ALWAYS_SHOW: "Mode Switched To 「Always Visible」",
      },
    },
    MODE_TO_MENU_TEXT_KEY_MAP: {
      ["Focus"]: "MENU_CMD_FOCUS",
      ["Hover"]: "MENU_CMD_HOVER",
      ["DoubleClick"]: "MENU_CMD_DBLCLICK",
      ["AlwaysShow"]: "MENU_CMD_ALWAYS_SHOW",
    },
    STORAGE_KEYS: {
      MODE_KEY: "PasswordDisplayMode",
    },
    MODE_TO_ALERT_MESSAGE_KEY_MAP: {
      ["Focus"]: "ALERT_MSG_FOCUS",
      ["Hover"]: "ALERT_MSG_HOVER",
      ["DoubleClick"]: "ALERT_MSG_DBLCLICK",
      ["AlwaysShow"]: "ALERT_MSG_ALWAYS_SHOW",
    },
  };

  const State = {
    currentMode: Config.MODES.FOCUS,
    currentLocale: "en-US",
    localizedStrings: Config.UI_TEXTS["en-US"],

    loadAndSetInitialState() {
      this.currentLocale = this.detectUserLanguage();
      this.localizedStrings =
        Config.UI_TEXTS[this.currentLocale] || Config.UI_TEXTS["en-US"];
      this.loadDisplayMode();
    },

    detectUserLanguage() {
      const languages = navigator.languages || [navigator.language];
      for (const lang of languages) {
        const langLower = lang.toLowerCase();
        if (langLower === "zh-cn") return "zh-CN";
        if (
          langLower === "zh-tw" ||
          langLower === "zh-hk" ||
          langLower === "zh-mo" ||
          langLower === "zh-hant"
        )
          return "zh-TW";
        if (langLower === "en-us") return "en-US";
        if (langLower.startsWith("zh-")) return "zh-CN";
        if (langLower.startsWith("en-")) return "en-US";
      }
      for (const lang of languages) {
        const langLower = lang.toLowerCase();
        if (langLower.startsWith("zh")) return "zh-CN";
        if (langLower.startsWith("en")) return "en-US";
      }
      return "en-US";
    },

    getLocalizedString(key, fallbackLang = "en-US") {
      const primaryLangData =
        this.localizedStrings || Config.UI_TEXTS[fallbackLang];
      const fallbackLangData = Config.UI_TEXTS[fallbackLang];
      return primaryLangData[key] ?? fallbackLangData[key] ?? `${key}?`;
    },

    loadDisplayMode() {
      let storedValue;
      try {
        storedValue = GM_getValue(
          Config.STORAGE_KEYS.MODE_KEY,
          Config.MODES.FOCUS
        );
      } catch (e) {
        storedValue = Config.MODES.FOCUS;
      }
      if (!Config.VALID_MODES.includes(storedValue)) {
        storedValue = Config.MODES.FOCUS;
      }
      this.currentMode = storedValue;
    },

    saveDisplayMode() {
      try {
        GM_setValue(Config.STORAGE_KEYS.MODE_KEY, this.currentMode);
      } catch (e) {}
    },

    setMode(newMode) {
      if (
        this.currentMode === newMode ||
        !Config.VALID_MODES.includes(newMode)
      ) {
        return false;
      }
      this.currentMode = newMode;
      this.saveDisplayMode();
      return true;
    },
  };

  const UserInterface = {
    notificationTimer: null,
    notificationRemovalTimer: null,
    registeredMenuCommandIds: [],

    injectCoreStyles() {
      const easeOutQuint = "cubic-bezier(0.23, 1, 0.32, 1)";
      const animationDuration = Config.SCRIPT_SETTINGS.ANIMATION_DURATION_MS;

      const baseCSS = `
          :root {
            --ctp-frappe-rosewater: rgb(242, 213, 207);
            --ctp-frappe-flamingo: rgb(238, 190, 190);
            --ctp-frappe-pink: rgb(244, 184, 228);
            --ctp-frappe-mauve: rgb(202, 158, 230);
            --ctp-frappe-red: rgb(231, 130, 132);
            --ctp-frappe-maroon: rgb(234, 153, 156);
            --ctp-frappe-peach: rgb(239, 159, 118);
            --ctp-frappe-yellow: rgb(229, 200, 144);
            --ctp-frappe-green: rgb(166, 209, 137);
            --ctp-frappe-teal: rgb(129, 200, 190);
            --ctp-frappe-sky: rgb(153, 209, 219);
            --ctp-frappe-sapphire: rgb(133, 193, 220);
            --ctp-frappe-blue: rgb(140, 170, 238);
            --ctp-frappe-lavender: rgb(186, 187, 241);
            --ctp-frappe-text: rgb(198, 208, 245);
            --ctp-frappe-subtext1: rgb(181, 191, 226);
            --ctp-frappe-subtext0: rgb(165, 173, 206);
            --ctp-frappe-overlay2: rgb(148, 156, 187);
            --ctp-frappe-overlay1: rgb(131, 139, 167);
            --ctp-frappe-overlay0: rgb(115, 121, 148);
            --ctp-frappe-surface2: rgb(98, 104, 128);
            --ctp-frappe-surface1: rgb(81, 87, 109);
            --ctp-frappe-surface0: rgb(65, 69, 89);
            --ctp-frappe-base: rgb(48, 52, 70);
            --ctp-frappe-mantle: rgb(41, 44, 60);
            --ctp-frappe-crust: rgb(35, 38, 52);

            --ctp-latte-rosewater: rgb(220, 138, 120);
            --ctp-latte-flamingo: rgb(221, 120, 120);
            --ctp-latte-pink: rgb(234, 118, 203);
            --ctp-latte-mauve: rgb(136, 57, 239);
            --ctp-latte-red: rgb(210, 15, 57);
            --ctp-latte-maroon: rgb(230, 69, 83);
            --ctp-latte-peach: rgb(254, 100, 11);
            --ctp-latte-yellow: rgb(223, 142, 29);
            --ctp-latte-green: rgb(64, 160, 43);
            --ctp-latte-teal: rgb(23, 146, 153);
            --ctp-latte-sky: rgb(4, 165, 229);
            --ctp-latte-sapphire: rgb(32, 159, 181);
            --ctp-latte-blue: rgb(30, 102, 245);
            --ctp-latte-lavender: rgb(114, 135, 253);
            --ctp-latte-text: rgb(76, 79, 105);
            --ctp-latte-subtext1: rgb(92, 95, 119);
            --ctp-latte-subtext0: rgb(108, 111, 133);
            --ctp-latte-overlay2: rgb(124, 127, 147);
            --ctp-latte-overlay1: rgb(140, 143, 161);
            --ctp-latte-overlay0: rgb(156, 160, 176);
            --ctp-latte-surface2: rgb(172, 176, 190);
            --ctp-latte-surface1: rgb(188, 192, 204);
            --ctp-latte-surface0: rgb(204, 208, 218);
            --ctp-latte-base: rgb(239, 241, 245);
            --ctp-latte-mantle: rgb(230, 233, 239);
            --ctp-latte-crust: rgb(220, 224, 232);

            --pr-notify-bg-dark: rgb(from var(--ctp-frappe-base) r g b / 0.85);
            --pr-notify-text-dark: var(--ctp-frappe-text);
            --pr-notify-border-dark: rgb(from var(--ctp-frappe-surface2) r g b / 0.25);
            --pr-notify-dot-color-dark: var(--ctp-frappe-green); /* Renamed from --ctp-frappe-green for clarity */
            --pr-notify-dot-glow-dark: rgb(from var(--ctp-frappe-green) r g b / 0.35); /* Glow for dot */


            --pr-notify-bg-light: rgb(from var(--ctp-latte-base) r g b / 0.85);
            --pr-notify-text-light: var(--ctp-latte-text);
            --pr-notify-border-light: rgb(from var(--ctp-latte-surface2) r g b / 0.25);
            --pr-notify-dot-color-light: var(--ctp-latte-green); /* Renamed from --ctp-latte-green for clarity */
            --pr-notify-dot-glow-light: rgb(from var(--ctp-latte-green) r g b / 0.35); /* Glow for dot */

            --pr-shadow-dark:
              0 1px 2px rgba(0, 0, 0, 0.1),
              0 6px 12px rgba(0, 0, 0, 0.2);
            --pr-shadow-light:
              0 1px 2px rgba(90, 90, 90, 0.06),
              0 6px 12px rgba(90, 90, 90, 0.12);
          }

          @keyframes pr-breathing-animation {
            0%, 100% {
              transform: scale(0.85);
              opacity: 0.7;
            }
            50% {
              transform: scale(1);
              opacity: 1;
            }
          }

          #${Config.ELEMENT_IDS.MODE_NOTIFICATION} {
            position: fixed;
            bottom: 20px;
            left: 50%;
            z-index: 2147483646;
            display: flex;
            align-items: center;
            padding: 10px 16px;
            border: 1px solid var(--pr-notify-border-dark);
            border-radius: 20px;
            background-color: var(--pr-notify-bg-dark);
            color: var(--pr-notify-text-dark);
            box-shadow: var(--pr-shadow-dark);
            box-sizing: border-box;
            opacity: 0;
            font-family: ${Config.SCRIPT_SETTINGS.UI_FONT_STACK};
            text-align: left;
            backdrop-filter: blur(16px) saturate(180%);
            -webkit-backdrop-filter: blur(16px) saturate(180%);
            transform: translate(-50%, calc(100% + 40px));
            transition: transform ${animationDuration}ms ${easeOutQuint},
                        opacity ${animationDuration * 0.8}ms ${easeOutQuint};
          }

          #${Config.ELEMENT_IDS.MODE_NOTIFICATION}.${
        Config.CSS_CLASSES.MODE_NOTIFICATION_VISIBLE
      } {
            transform: translate(-50%, 0);
            opacity: 1;
          }

          #${Config.ELEMENT_IDS.MODE_NOTIFICATION} .${
        Config.CSS_CLASSES.BREATHING_DOT
      } {
            width: 8px;
            height: 8px;
            margin-right: 10px;
            border-radius: 50%;
            background-color: var(--pr-notify-dot-color-dark);
            box-shadow: 0 0 8px 3px var(--pr-notify-dot-glow-dark); /* Added glow */
            flex-shrink: 0;
            animation: pr-breathing-animation 2000ms ease-in-out infinite;
            /* No transition needed here as dot color doesn't change based on state for PR */
          }

          #${Config.ELEMENT_IDS.MODE_NOTIFICATION} .${
        Config.CSS_CLASSES.MODE_NOTIFICATION_MESSAGE
      } {
            color: var(--pr-notify-text-dark);
            font-size: 13px;
            font-weight: 500;
            line-height: 1.2;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
          }

          @media (prefers-color-scheme: light) {
            #${Config.ELEMENT_IDS.MODE_NOTIFICATION} {
              border: 1px solid var(--pr-notify-border-light);
              background-color: var(--pr-notify-bg-light);
              color: var(--pr-notify-text-light);
              box-shadow: var(--pr-shadow-light);
            }
            #${Config.ELEMENT_IDS.MODE_NOTIFICATION} .${
        Config.CSS_CLASSES.BREATHING_DOT
      } {
               background-color: var(--pr-notify-dot-color-light);
               box-shadow: 0 0 8px 3px var(--pr-notify-dot-glow-light); /* Added glow for light mode */
            }
            #${Config.ELEMENT_IDS.MODE_NOTIFICATION} .${
        Config.CSS_CLASSES.MODE_NOTIFICATION_MESSAGE
      } {
              color: var(--pr-notify-text-light);
            }
          }
        `;
      try {
        GM_addStyle(baseCSS);
      } catch (e) {}
    },

    displayModeNotification(messageKey) {
      if (this.notificationTimer) clearTimeout(this.notificationTimer);
      if (this.notificationRemovalTimer)
        clearTimeout(this.notificationRemovalTimer);
      this.notificationTimer = null;
      this.notificationRemovalTimer = null;

      const message = State.getLocalizedString(messageKey) || messageKey;

      const renderNotification = () => {
        let notificationElement = document.getElementById(
          Config.ELEMENT_IDS.MODE_NOTIFICATION
        );
        if (!notificationElement && document.body) {
          notificationElement = document.createElement("div");
          notificationElement.id = Config.ELEMENT_IDS.MODE_NOTIFICATION;
          notificationElement.innerHTML = `
            <div class="${Config.CSS_CLASSES.BREATHING_DOT}"></div>
            <div class="${Config.CSS_CLASSES.MODE_NOTIFICATION_MESSAGE}"></div>
          `.trim();
          document.body.appendChild(notificationElement);
        } else if (!notificationElement) {
          return;
        }

        const messageElement = notificationElement.querySelector(
          `.${Config.CSS_CLASSES.MODE_NOTIFICATION_MESSAGE}`
        );

        if (messageElement) messageElement.textContent = message;

        // PR script's dot color does not change based on a disabled class,
        // so no need to add/remove a .disabled class here for the dot.
        // The glow is always based on the primary dot color (green).

        notificationElement.classList.remove(
          Config.CSS_CLASSES.MODE_NOTIFICATION_VISIBLE
        );
        void notificationElement.offsetWidth;

        requestAnimationFrame(() => {
          const currentElement = document.getElementById(
            Config.ELEMENT_IDS.MODE_NOTIFICATION
          );
          if (currentElement) {
            currentElement.classList.add(
              Config.CSS_CLASSES.MODE_NOTIFICATION_VISIBLE
            );
          }
        });

        this.notificationTimer = setTimeout(() => {
          const currentElement = document.getElementById(
            Config.ELEMENT_IDS.MODE_NOTIFICATION
          );
          if (currentElement) {
            currentElement.classList.remove(
              Config.CSS_CLASSES.MODE_NOTIFICATION_VISIBLE
            );
            this.notificationRemovalTimer = setTimeout(() => {
              document
                .getElementById(Config.ELEMENT_IDS.MODE_NOTIFICATION)
                ?.remove();
              this.notificationTimer = null;
              this.notificationRemovalTimer = null;
            }, Config.SCRIPT_SETTINGS.ANIMATION_DURATION_MS);
          } else {
            this.notificationTimer = null;
            this.notificationRemovalTimer = null;
          }
        }, Config.SCRIPT_SETTINGS.NOTIFICATION_VISIBILITY_DURATION_MS);
      };
      renderNotification();
    },

    updateUserScriptMenuCommands() {
      this.registeredMenuCommandIds.forEach((id) => {
        try {
          GM_unregisterMenuCommand(id);
        } catch (e) {}
      });
      this.registeredMenuCommandIds = [];

      Config.VALID_MODES.forEach((mode) => {
        const menuKey = Config.MODE_TO_MENU_TEXT_KEY_MAP[mode];
        const baseText = State.getLocalizedString(menuKey);
        const commandText =
          baseText + (mode === State.currentMode ? " ✅" : "");

        try {
          const commandId = GM_registerMenuCommand(commandText, () =>
            ScriptManager.setModeAndUpdate(mode)
          );
          this.registeredMenuCommandIds.push(commandId);
        } catch (e) {}
      });
    },
  };

  const InputManager = {
    processPasswordInput(input, mode) {
      if (
        !(input instanceof HTMLInputElement) ||
        input.type === "hidden" ||
        input.getAttribute(Config.ATTRIBUTES.PROCESSED) === mode
      ) {
        return;
      }

      if (mode === Config.MODES.ALWAYS_SHOW) {
        input.type = "text";
      } else {
        if (input.type !== "password") {
          input.type = "password";
        }
      }
      input.setAttribute(Config.ATTRIBUTES.PROCESSED, mode);
    },

    findAndProcessNewInputs(rootNode, mode) {
      if (!rootNode || typeof rootNode.querySelectorAll !== "function") return;
      try {
        const query = `input[type="password"]:not([${Config.ATTRIBUTES.PROCESSED}="${mode}"]), input[${Config.ATTRIBUTES.PROCESSED}]:not([${Config.ATTRIBUTES.PROCESSED}="${mode}"])`;
        rootNode.querySelectorAll(query).forEach((input) => {
          this.processPasswordInput(input, mode);
        });

        const elementsToCheckForShadow =
          rootNode === document ||
          rootNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
            ? rootNode.querySelectorAll("*")
            : [rootNode];

        elementsToCheckForShadow.forEach((el) => {
          if (
            el.shadowRoot &&
            typeof el.shadowRoot.querySelectorAll === "function"
          ) {
            this.findAndProcessNewInputs(el.shadowRoot, mode);
          }
        });
      } catch (e) {}
    },

    applyCurrentModeToAllInputs() {
      try {
        this.findAndProcessNewInputs(document, State.currentMode);
        document.querySelectorAll("*").forEach((el) => {
          if (el.shadowRoot) {
            this.findAndProcessNewInputs(el.shadowRoot, State.currentMode);
          }
        });
      } catch (e) {}
    },
  };

  const EventManager = {
    domMutationObserver: null,

    handleShowPasswordOnHover(event) {
      const input = event.target;
      if (
        State.currentMode === Config.MODES.HOVER &&
        input instanceof HTMLInputElement &&
        input.matches(
          `input[type="password"][${Config.ATTRIBUTES.PROCESSED}="${Config.MODES.HOVER}"]`
        )
      ) {
        input.type = "text";
      }
    },

    handleHidePasswordOnLeave(event) {
      const input = event.target;
      if (
        State.currentMode === Config.MODES.HOVER &&
        input instanceof HTMLInputElement &&
        input.matches(
          `input[type="text"][${Config.ATTRIBUTES.PROCESSED}="${Config.MODES.HOVER}"]`
        )
      ) {
        input.type = "password";
      }
    },

    handleTogglePasswordOnDoubleClick(event) {
      const input = event.target;
      if (
        State.currentMode === Config.MODES.DBLCLICK &&
        input instanceof HTMLInputElement &&
        input.matches(
          `input[${Config.ATTRIBUTES.PROCESSED}="${Config.MODES.DBLCLICK}"]`
        )
      ) {
        input.type = input.type === "password" ? "text" : "password";
      }
    },

    handleFocusIn(event) {
      const input = event.target;
      if (
        State.currentMode === Config.MODES.FOCUS &&
        input instanceof HTMLInputElement &&
        input.matches(
          `input[type="password"][${Config.ATTRIBUTES.PROCESSED}="${Config.MODES.FOCUS}"]`
        )
      ) {
        input.type = "text";
      }
    },

    handleFocusOut(event) {
      const input = event.target;
      if (
        State.currentMode === Config.MODES.FOCUS &&
        input instanceof HTMLInputElement &&
        input.matches(
          `input[type="text"][${Config.ATTRIBUTES.PROCESSED}="${Config.MODES.FOCUS}"]`
        )
      ) {
        input.type = "password";
      }
    },

    handleKeyboardShortcut(event) {
      if (
        (event.ctrlKey || event.metaKey) &&
        event.altKey &&
        !event.shiftKey &&
        event.code === "KeyP"
      ) {
        event.preventDefault();
        event.stopPropagation();

        const currentIndex = Config.VALID_MODES.indexOf(State.currentMode);
        const nextIndex = (currentIndex + 1) % Config.VALID_MODES.length;
        const nextMode = Config.VALID_MODES[nextIndex];
        ScriptManager.setModeAndUpdate(nextMode);
      }
    },

    handleDOMMutation(mutationsList) {
      for (const mutation of mutationsList) {
        if (mutation.type === "childList") {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType !== Node.ELEMENT_NODE) return;
            try {
              InputManager.findAndProcessNewInputs(node, State.currentMode);
            } catch (e) {}
          });
        } else if (
          mutation.type === "attributes" &&
          mutation.attributeName === "type"
        ) {
          const targetInput = mutation.target;
          if (
            targetInput.nodeType === Node.ELEMENT_NODE &&
            targetInput.matches &&
            targetInput.matches('input[type="password"]') &&
            targetInput.getAttribute(Config.ATTRIBUTES.PROCESSED) !==
              State.currentMode
          ) {
            try {
              InputManager.processPasswordInput(targetInput, State.currentMode);
            } catch (e) {}
          }
        }
      }
    },

    initializeDOMObserver() {
      if (this.domMutationObserver) return;

      const observerOptions = {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ["type"],
      };
      this.domMutationObserver = new MutationObserver(
        this.handleDOMMutation.bind(this)
      );

      if (document.body) {
        try {
          this.domMutationObserver.observe(document.body, observerOptions);
        } catch (error) {
          this.domMutationObserver = null;
        }
      } else {
        document.addEventListener(
          "DOMContentLoaded",
          () => {
            if (document.body) {
              try {
                this.domMutationObserver.observe(
                  document.body,
                  observerOptions
                );
              } catch (error) {
                this.domMutationObserver = null;
              }
            }
          },
          { once: true }
        );
      }
    },

    initializeGlobalEventListeners() {
      document.body.addEventListener(
        "mouseenter",
        this.handleShowPasswordOnHover.bind(this),
        true
      );
      document.body.addEventListener(
        "mouseleave",
        this.handleHidePasswordOnLeave.bind(this),
        true
      );
      document.body.addEventListener(
        "dblclick",
        this.handleTogglePasswordOnDoubleClick.bind(this)
      );
      document.addEventListener("focus", this.handleFocusIn.bind(this), true);
      document.addEventListener("blur", this.handleFocusOut.bind(this), true);
      document.addEventListener(
        "keydown",
        this.handleKeyboardShortcut.bind(this),
        true
      );
    },

    init() {
      this.initializeGlobalEventListeners();
      this.initializeDOMObserver();
    },
  };

  const ScriptManager = {
    init() {
      try {
        UserInterface.injectCoreStyles();
        State.loadAndSetInitialState();
        UserInterface.updateUserScriptMenuCommands();
        InputManager.applyCurrentModeToAllInputs();
        EventManager.init();
      } catch (error) {}
    },

    setModeAndUpdate(newMode) {
      if (State.setMode(newMode)) {
        const alertMessageKey =
          Config.MODE_TO_ALERT_MESSAGE_KEY_MAP[State.currentMode];
        UserInterface.displayModeNotification(alertMessageKey);
        InputManager.applyCurrentModeToAllInputs();
        UserInterface.updateUserScriptMenuCommands();
      }
    },
  };

  ScriptManager.init();
})();