// ==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 // @downloadURL none // ==/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 = `
${getLocalizedText("ScriptTitle")}
${message}
`; 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(); })();