密码显示助手

通过鼠标悬浮/双击/始终显示来显示密码框内容 可通过脚本菜单选择触发方式

当前为 2025-04-16 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴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        Reveal Passwords By Hovering/DoubleClicking/Always Show Select Mode Via The Tampermonkey Menu
// @description:zh-CN  通过鼠标悬浮/双击/始终显示来显示密码框内容 可通过脚本菜单选择触发方式
// @description:zh-TW  透過滑鼠懸浮/雙擊/始終顯示來顯示密碼框內容 可透過腳本選單選擇觸發方式
// @version            1.2.0
// @icon               https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/PasswordRevealerIcon.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
// ==/UserScript==

(function () {
  "use strict";

  const ModeKey = "PasswordDisplayMode";
  const ModeHover = "Hover";
  const ModeDBClick = "DoubleClick";
  const ModeAlwaysShow = "AlwaysShow";
  const NotificationId = "PasswordRevealerNotification";
  const NotificationTimeout = 2000;
  const AnimationDuration = 300;
  const ScriptIconUrl =
    "https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/PasswordRevealerIcon.svg";
  const ProcessedAttribute = "DataPasswordRevealerProcessed";

  const Localization = {
    "en-US": {
      ScriptTitle: "Password Revealer",
      MenuCmdSetHover: "「Hover」Mode",
      MenuCmdSetDBClick: "「Double Click」Mode",
      MenuCmdSetAlwaysShow: "「Always Show」Mode",
      AlertMessages: {
        [ModeHover]: "Mode Switched To 「Hover」",
        [ModeDBClick]: "Mode Switched To 「Double Click」",
        [ModeAlwaysShow]: "Mode Switched To 「Always Show」",
      },
    },
    "zh-CN": {
      ScriptTitle: "密码显示助手",
      MenuCmdSetHover: "「悬浮显示」模式",
      MenuCmdSetDBClick: "「双击切换」模式",
      MenuCmdSetAlwaysShow: "「始终显示」模式",
      AlertMessages: {
        [ModeHover]: "模式已切换为「悬浮显示」",
        [ModeDBClick]: "模式已切换为「双击切换」",
        [ModeAlwaysShow]: "模式已切换为「始终显示」",
      },
    },
    "zh-TW": {
      ScriptTitle: "密碼顯示助手",
      MenuCmdSetHover: "「懸浮顯示」模式",
      MenuCmdSetDBClick: "「雙擊切換」模式",
      MenuCmdSetAlwaysShow: "「始終顯示」模式",
      AlertMessages: {
        [ModeHover]: "模式已切換為「懸浮顯示」",
        [ModeDBClick]: "模式已切換為「雙擊切換」",
        [ModeAlwaysShow]: "模式已切換為「始終顯示」",
      },
    },
  };

  const ModeMenuTextKeys = {
    [ModeHover]: "MenuCmdSetHover",
    [ModeDBClick]: "MenuCmdSetDBClick",
    [ModeAlwaysShow]: "MenuCmdSetAlwaysShow",
  };

  let RegisteredMenuCommandIds = [];

  function GetLanguageKey() {
    const Lang = navigator.language;
    if (Lang.startsWith("zh")) {
      return Lang === "zh-TW" || Lang === "zh-HK" || Lang === "zh-Hant"
        ? "zh-TW"
        : "zh-CN";
    }
    return "en-US";
  }

  function GetLocalizedText(Key, SubKey = null, FallbackLang = "en-US") {
    const LangKey = GetLanguageKey();
    const PrimaryLangData = Localization[LangKey] || Localization[FallbackLang];
    const FallbackLangData = Localization[FallbackLang];

    let Value;
    if (SubKey && Key === "AlertMessages") {
      Value = PrimaryLangData[Key]?.[SubKey] ?? FallbackLangData[Key]?.[SubKey];
    } else {
      Value = PrimaryLangData[Key] ?? FallbackLangData[Key];
    }
    return Value ?? (SubKey ? `${Key}.${SubKey}?` : `${Key}?`);
  }

  function InjectNotificationStyles() {
    GM_addStyle(`
      #${NotificationId} {
        position: fixed;
        top: 20px;
        right: -400px;
        width: 300px;
        background-color: rgba(240, 240, 240, 0.9);
        color: #333;
        padding: 10px;
        border-radius: 10px;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        z-index: 99999;
        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
        display: flex;
        align-items: flex-start;
        opacity: 0;
        transition: right ${AnimationDuration}ms ease-out, opacity ${
      AnimationDuration * 0.8
    }ms ease-out;
        box-sizing: border-box;
        backdrop-filter: blur(8px);
        -webkit-backdrop-filter: blur(8px);
      }
      #${NotificationId}.visible {
        right: 20px;
        opacity: 1;
      }
      #${NotificationId} .pr-icon {
        width: 32px;
        height: 32px;
        margin-right: 10px;
        flex-shrink: 0;
      }
      #${NotificationId} .pr-content {
        display: flex;
        flex-direction: column;
        flex-grow: 1;
        min-width: 0;
      }
      #${NotificationId} .pr-title {
        font-size: 13px;
        font-weight: 600;
        margin-bottom: 2px;
        color: #111;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      #${NotificationId} .pr-message {
        font-size: 12px;
        line-height: 1.3;
        color: #444;
         word-wrap: break-word;
         overflow-wrap: break-word;
      }
      @media (prefers-color-scheme: dark) {
          #${NotificationId} {
              background-color: rgba(50, 50, 50, 0.85);
              color: #eee;
              box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
          }
          #${NotificationId} .pr-title {
              color: #f0f0f0;
          }
          #${NotificationId} .pr-message {
               color: #ccc;
          }
      }
    `);
  }

  let NotificationTimer = null;
  let RemovalTimer = null;

  function ShowNotification(Message) {
    if (NotificationTimer) clearTimeout(NotificationTimer);
    if (RemovalTimer) clearTimeout(RemovalTimer);

    const ExistingNotification = document.getElementById(NotificationId);
    if (ExistingNotification) {
      ExistingNotification.remove();
    }

    const NotificationElement = document.createElement("div");
    NotificationElement.id = NotificationId;
    NotificationElement.innerHTML = `
      <img src="${ScriptIconUrl}" alt="Icon" class="pr-icon">
      <div class="pr-content">
        <div class="pr-title">${GetLocalizedText("ScriptTitle")}</div>
        <div class="pr-message">${Message}</div>
      </div>
    `;

    document.body.appendChild(NotificationElement);

    requestAnimationFrame(() => {
      NotificationElement.classList.add("visible");
    });

    NotificationTimer = setTimeout(() => {
      NotificationElement.classList.remove("visible");
      RemovalTimer = setTimeout(() => {
        if (NotificationElement.parentNode) {
          NotificationElement.remove();
        }
        NotificationTimer = null;
        RemovalTimer = null;
      }, AnimationDuration);
    }, NotificationTimeout);
  }

  function ShowPasswordOnHover() {
    this.type = "text";
  }

  function HidePasswordOnLeave() {
    this.type = "password";
  }

  function TogglePasswordOnDoubleClick() {
    this.type = this.type === "password" ? "text" : "password";
  }

  function ApplyHoverBehavior(Input) {
    Input.addEventListener("mouseenter", ShowPasswordOnHover);
    Input.addEventListener("mouseleave", HidePasswordOnLeave);
    Input.removeEventListener("dblclick", TogglePasswordOnDoubleClick);
  }

  function RemoveHoverBehavior(Input) {
    Input.removeEventListener("mouseenter", ShowPasswordOnHover);
    Input.removeEventListener("mouseleave", HidePasswordOnLeave);
  }

  function ApplyDoubleClickBehavior(Input) {
    Input.addEventListener("dblclick", TogglePasswordOnDoubleClick);
    Input.removeEventListener("mouseenter", ShowPasswordOnHover);
    Input.removeEventListener("mouseleave", HidePasswordOnLeave);
  }

  function RemoveDoubleClickBehavior(Input) {
    Input.removeEventListener("dblclick", TogglePasswordOnDoubleClick);
  }

  function ProcessPasswordInput(Input, Mode) {
    if (!(Input instanceof HTMLInputElement)) return;

    RemoveHoverBehavior(Input);
    RemoveDoubleClickBehavior(Input);

    switch (Mode) {
      case ModeHover:
        Input.type = "password";
        ApplyHoverBehavior(Input);
        break;
      case ModeDBClick:
        Input.type = "password";
        ApplyDoubleClickBehavior(Input);
        break;
      case ModeAlwaysShow:
        Input.type = "text";
        break;
      default:
        Input.type = "password";
        ApplyHoverBehavior(Input);
        Mode = ModeHover;
    }

    Input.setAttribute(ProcessedAttribute, Mode);
  }

  function SetMode(NewMode) {
    if (
      NewMode === CurrentMode ||
      ![ModeHover, ModeDBClick, ModeAlwaysShow].includes(NewMode)
    ) {
      return;
    }

    CurrentMode = NewMode;
    GM_setValue(ModeKey, CurrentMode);

    const AlertMessage = GetLocalizedText("AlertMessages", CurrentMode);
    ShowNotification(AlertMessage);

    const AllPasswordInputs = document.querySelectorAll(
      `input[type="password"], input[type="text"][${ProcessedAttribute}]`
    );
    AllPasswordInputs.forEach((Input) => {
      if (Input.hasAttribute(ProcessedAttribute) || Input.type === "password") {
        ProcessPasswordInput(Input, CurrentMode);
      }
    });

    RegisterModeMenuCommands();
  }

  function RegisterModeMenuCommands() {
    RegisteredMenuCommandIds.forEach((Id) => {
      try {
        GM_unregisterMenuCommand(Id);
      } catch (E) {}
    });
    RegisteredMenuCommandIds = [];

    const ModesToRegister = [ModeHover, ModeDBClick, ModeAlwaysShow];

    ModesToRegister.forEach((Mode) => {
      const MenuKey = ModeMenuTextKeys[Mode];
      const BaseText = GetLocalizedText(MenuKey);
      const CommandText = BaseText + (Mode === CurrentMode ? " ✅" : "");

      const CommandId = GM_registerMenuCommand(CommandText, () =>
        SetMode(Mode)
      );
      RegisteredMenuCommandIds.push(CommandId);
    });
  }

  let CurrentMode = GM_getValue(ModeKey, ModeHover);

  if (![ModeHover, ModeDBClick, ModeAlwaysShow].includes(CurrentMode)) {
    CurrentMode = ModeHover;
    GM_setValue(ModeKey, CurrentMode);
  }

  InjectNotificationStyles();

  document
    .querySelectorAll('input[type="password"]')
    .forEach((Input) => ProcessPasswordInput(Input, CurrentMode));

  const ObserverCallback = (MutationsList) => {
    for (const Mutation of MutationsList) {
      if (Mutation.type === "childList" && Mutation.addedNodes.length > 0) {
        Mutation.addedNodes.forEach((Node) => {
          if (
            Node.nodeType === Node.ELEMENT_NODE &&
            Node.matches &&
            Node.matches('input[type="password"]') &&
            !Node.hasAttribute(ProcessedAttribute)
          ) {
            ProcessPasswordInput(Node, CurrentMode);
          } else if (
            Node.nodeType === Node.ELEMENT_NODE &&
            Node.querySelectorAll
          ) {
            const DescendantInputs = Node.querySelectorAll(
              `input[type="password"]:not([${ProcessedAttribute}])`
            );
            DescendantInputs.forEach((Input) =>
              ProcessPasswordInput(Input, CurrentMode)
            );
          }
        });
      } 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.hasAttribute(ProcessedAttribute)
        ) {
          ProcessPasswordInput(TargetInput, CurrentMode);
        }
      }
    }
  };

  const Observer = new MutationObserver(ObserverCallback);
  Observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ["type"],
  });

  RegisterModeMenuCommands();
})();