密码显示助手

密码输入框内容可聚焦即显 / 悬浮即览 / 双击切换 / 始终可见 | 通过菜单或快捷键(Meta/Ctrl+Alt+P)切换显示模式

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();