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