Greasy Fork

Greasy Fork is available in English.

密码显示助手

通过鼠标悬浮/双击/始终显示来显示密码框内容 可通过脚本菜单或快捷键 Ctrl/Meta+Alt+P 选择触发方式

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴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 Or Shortcut Ctrl/Meta+Alt+P.
// @description:zh-CN  通过鼠标悬浮/双击/始终显示来显示密码框内容 可通过脚本菜单或快捷键 Ctrl/Meta+Alt+P 选择触发方式
// @description:zh-TW  透過滑鼠懸浮/雙擊/始終顯示來顯示密碼框內容 可透過腳本選單或快捷鍵 Ctrl/Meta+Alt+P 選擇觸發方式
// @version            1.3.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
// @run-at             document-idle
// ==/UserScript==

(function () {
  "use strict";

  const MODE_KEY = "PasswordDisplayMode";
  const MODE_HOVER = "Hover";
  const MODE_DBLCLICK = "DoubleClick";
  const MODE_ALWAYS_SHOW = "AlwaysShow";
  const NOTIFICATION_ID = "PasswordRevealerNotification";
  const NOTIFICATION_TIMEOUT = 2000;
  const ANIMATION_DURATION = 300;
  const SCRIPT_ICON_URL =
    "https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/PasswordRevealerIcon.svg";
  const PROCESSED_ATTRIBUTE = "data-password-revealer-processed";

  const VALID_MODES = [MODE_HOVER, MODE_DBLCLICK, MODE_ALWAYS_SHOW];

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

  const MODE_MENU_TEXT_KEYS = {
    [MODE_HOVER]: "MenuCmdSetHover",
    [MODE_DBLCLICK]: "MenuCmdSetDBClick",
    [MODE_ALWAYS_SHOW]: "MenuCmdSetAlwaysShow",
  };

  let registeredMenuCommandIds = [];
  let notificationTimer = null;
  let removalTimer = null;
  let currentMode = GM_getValue(MODE_KEY, MODE_HOVER);

  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(`
      #${NOTIFICATION_ID} {
        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 ${ANIMATION_DURATION}ms ease-out, opacity ${
      ANIMATION_DURATION * 0.8
    }ms ease-out;
        box-sizing: border-box;
        backdrop-filter: blur(8px);
        -webkit-backdrop-filter: blur(8px);
      }

      #${NOTIFICATION_ID}.visible {
        right: 20px;
        opacity: 1;
      }

      #${NOTIFICATION_ID} .pr-icon {
        width: 32px;
        height: 32px;
        margin-right: 10px;
        flex-shrink: 0;
      }

      #${NOTIFICATION_ID} .pr-content {
        display: flex;
        flex-direction: column;
        flex-grow: 1;
        min-width: 0;
      }

      #${NOTIFICATION_ID} .pr-title {
        font-size: 13px;
        font-weight: 600;
        margin-bottom: 2px;
        color: #111;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      #${NOTIFICATION_ID} .pr-message {
        font-size: 12px;
        line-height: 1.3;
        color: #444;
        word-wrap: break-word;
        overflow-wrap: break-word;
      }

      @media (prefers-color-scheme: dark) {
        #${NOTIFICATION_ID} {
          background-color: rgba(50, 50, 50, 0.85);
          color: #eee;
          box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }
        #${NOTIFICATION_ID} .pr-title {
          color: #f0f0f0;
        }
        #${NOTIFICATION_ID} .pr-message {
          color: #ccc;
        }
      }
    `);
  }

  function showNotification(message) {
    if (notificationTimer) clearTimeout(notificationTimer);
    if (removalTimer) clearTimeout(removalTimer);

    const existingNotification = document.getElementById(NOTIFICATION_ID);
    if (existingNotification) {
      existingNotification.remove();
    }

    const notificationElement = document.createElement("div");
    notificationElement.id = NOTIFICATION_ID;
    notificationElement.innerHTML = `
      <img src="${SCRIPT_ICON_URL}" alt="" 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;
      }, ANIMATION_DURATION);
    }, NOTIFICATION_TIMEOUT);
  }

  function processPasswordInput(input, mode) {
    if (
      !(input instanceof HTMLInputElement) ||
      input.type === "hidden" ||
      input.hasAttribute(PROCESSED_ATTRIBUTE)
    ) {
      return;
    }

    if (mode === MODE_ALWAYS_SHOW) {
      input.type = "text";
    } else {
      if (input.type !== "password") {
        input.type = "password";
      }
    }
    input.setAttribute(PROCESSED_ATTRIBUTE, mode);
  }

  function findAllPasswordInputs(rootNode) {
    const results = [];
    try {
      rootNode
        .querySelectorAll(
          `input[type="password"]:not([${PROCESSED_ATTRIBUTE}])`
        )
        .forEach((input) => results.push(input));

      rootNode.querySelectorAll("*").forEach((el) => {
        if (el.shadowRoot) {
          results.push(...findAllPasswordInputs(el.shadowRoot));
        }
      });
    } catch (e) {}
    return results;
  }

  function setMode(newMode) {
    if (currentMode === newMode || !VALID_MODES.includes(newMode)) {
      return;
    }

    const oldMode = currentMode;
    currentMode = newMode;
    GM_setValue(MODE_KEY, currentMode);

    const alertMessage = getLocalizedText("AlertMessages", currentMode);
    showNotification(alertMessage);

    const processedInputs = document.querySelectorAll(
      `input[${PROCESSED_ATTRIBUTE}]`
    );
    processedInputs.forEach((input) => {
      input.setAttribute(PROCESSED_ATTRIBUTE, currentMode);
      if (currentMode === MODE_ALWAYS_SHOW) {
        input.type = "text";
      } else if (oldMode === MODE_ALWAYS_SHOW) {
        input.type = "password";
      }
    });

    const untrackedPasswordInputs = findAllPasswordInputs(document.body);
    untrackedPasswordInputs.forEach((input) =>
      processPasswordInput(input, currentMode)
    );

    registerModeMenuCommands();
  }

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

    VALID_MODES.forEach((mode) => {
      const menuKey = MODE_MENU_TEXT_KEYS[mode];
      const baseText = getLocalizedText(menuKey);
      const commandText = baseText + (mode === currentMode ? " ✅" : "");

      const commandId = GM_registerMenuCommand(commandText, () =>
        setMode(mode)
      );
      registeredMenuCommandIds.push(commandId);
    });
  }

  function showPasswordOnHover(event) {
    const input = event.target;
    if (
      currentMode === MODE_HOVER &&
      input.matches(
        `input[type="password"][${PROCESSED_ATTRIBUTE}="${MODE_HOVER}"]`
      )
    ) {
      input.type = "text";
    }
  }

  function hidePasswordOnLeave(event) {
    const input = event.target;
    if (
      currentMode === MODE_HOVER &&
      input.matches(
        `input[type="text"][${PROCESSED_ATTRIBUTE}="${MODE_HOVER}"]`
      )
    ) {
      input.type = "password";
    }
  }

  function togglePasswordOnDoubleClick(event) {
    const input = event.target;
    if (
      currentMode === MODE_DBLCLICK &&
      input.matches(`input[${PROCESSED_ATTRIBUTE}="${MODE_DBLCLICK}"]`)
    ) {
      input.type = input.type === "password" ? "text" : "password";
    }
  }

  function initializeEventListeners() {
    document.body.addEventListener("mouseenter", showPasswordOnHover, true);
    document.body.addEventListener("mouseleave", hidePasswordOnLeave, true);
    document.body.addEventListener("dblclick", togglePasswordOnDoubleClick);
  }

  function handleKeyDown(event) {
    if (
      (event.ctrlKey || event.metaKey) &&
      event.altKey &&
      event.code === "KeyP"
    ) {
      event.preventDefault();
      event.stopPropagation();

      const currentIndex = VALID_MODES.indexOf(currentMode);
      const nextIndex = (currentIndex + 1) % VALID_MODES.length;
      const nextMode = VALID_MODES[nextIndex];

      setMode(nextMode);
    }
  }

  const observerCallback = (mutationsList) => {
    for (const mutation of mutationsList) {
      if (mutation.type === "childList") {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            const inputsToProcess = findAllPasswordInputs(node);
            inputsToProcess.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(PROCESSED_ATTRIBUTE)
        ) {
          processPasswordInput(targetInput, currentMode);
        }
      }
    }
  };

  const observer = new MutationObserver(observerCallback);

  if (!VALID_MODES.includes(currentMode)) {
    currentMode = MODE_HOVER;
    GM_setValue(MODE_KEY, currentMode);
  }

  injectNotificationStyles();
  findAllPasswordInputs(document.body).forEach((input) =>
    processPasswordInput(input, currentMode)
  );

  initializeEventListeners();
  document.addEventListener("keydown", handleKeyDown, true);

  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ["type"],
  });

  registerModeMenuCommands();
})();