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