密碼顯示助手

密碼輸入框內容可聚焦即顯 / 懸停即覽 / 雙擊切換 / 始終可見 | 透過選單或快速鍵(Meta/Ctrl+Alt+P)切換顯示模式

目前為 2025-05-24 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();