密码显示助手

通过鼠标悬浮或双击来显示密码框内容 可通过脚本菜单切换触发方式

目前为 2025-04-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Password Revealer
  3. // @name:zh-CN 密码显示助手
  4. // @name:zh-TW 密碼顯示助手
  5. // @description Reveal Passwords By Hovering Or DoubleClicking The Input Field Switch Modes Via The Tampermonkey Menu
  6. // @description:zh-CN 通过鼠标悬浮或双击来显示密码框内容 可通过脚本菜单切换触发方式
  7. // @description:zh-TW 透過滑鼠懸浮或雙擊來顯示密碼框內容 可透過腳本選單切換觸發方式
  8. // @version 1.1.0
  9. // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/PasswordRevealerIcon.svg
  10. // @author 念柚
  11. // @namespace https://github.com/MiPoNianYou/UserScripts
  12. // @supportURL https://github.com/MiPoNianYou/UserScripts/issues
  13. // @license GPL-3.0
  14. // @match *://*/*
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_addStyle
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. "use strict";
  23.  
  24. const ModeKey = "PasswordDisplayMode";
  25. const ModeHover = "Hover";
  26. const ModeDBClick = "DoubleClick";
  27. const NotificationId = "password-revealer-notification";
  28. const NotificationTimeout = 2000;
  29. const AnimationDuration = 300;
  30. const ScriptIconUrl =
  31. "https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/PasswordRevealerIcon.svg";
  32.  
  33. const ScriptTitles = {
  34. "en-US": "Password Revealer",
  35. "zh-CN": "密码显示助手",
  36. "zh-TW": "密碼顯示助手",
  37. };
  38.  
  39. const MenuCommandTexts = {
  40. "en-US": "Toggle Password Display Mode",
  41. "zh-CN": "切换密码显示模式",
  42. "zh-TW": "切換密碼顯示模式",
  43. };
  44.  
  45. const AlertMessages = {
  46. "en-US": {
  47. [ModeHover]: "Mode Switched To 「Hover」",
  48. [ModeDBClick]: "Mode Switched To 「Double Click」",
  49. },
  50. "zh-CN": {
  51. [ModeHover]: "模式已切换为:悬浮显示",
  52. [ModeDBClick]: "模式已切换为:双击切换",
  53. },
  54. "zh-TW": {
  55. [ModeHover]: "模式已切換為:懸浮顯示",
  56. [ModeDBClick]: "模式已切換為:雙擊切換",
  57. },
  58. };
  59.  
  60. function InjectNotificationStyles() {
  61. GM_addStyle(`
  62. #${NotificationId} {
  63. position: fixed;
  64. top: 20px;
  65. right: -400px;
  66. width: 300px;
  67. background-color: rgba(240, 240, 240, 0.9);
  68. color: #333;
  69. padding: 10px;
  70. border-radius: 10px;
  71. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  72. z-index: 99999;
  73. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
  74. display: flex;
  75. align-items: flex-start;
  76. opacity: 0;
  77. transition: right ${AnimationDuration}ms ease-out, opacity ${
  78. AnimationDuration * 0.8
  79. }ms ease-out;
  80. box-sizing: border-box;
  81. backdrop-filter: blur(8px);
  82. -webkit-backdrop-filter: blur(8px);
  83. }
  84. #${NotificationId}.visible {
  85. right: 20px;
  86. opacity: 1;
  87. }
  88. #${NotificationId} .pr-icon {
  89. width: 32px;
  90. height: 32px;
  91. margin-right: 10px;
  92. flex-shrink: 0;
  93. }
  94. #${NotificationId} .pr-content {
  95. display: flex;
  96. flex-direction: column;
  97. flex-grow: 1;
  98. min-width: 0;
  99. }
  100. #${NotificationId} .pr-title {
  101. font-size: 13px;
  102. font-weight: 600;
  103. margin-bottom: 2px;
  104. color: #111;
  105. white-space: nowrap;
  106. overflow: hidden;
  107. text-overflow: ellipsis;
  108. }
  109. #${NotificationId} .pr-message {
  110. font-size: 12px;
  111. line-height: 1.3;
  112. color: #444;
  113. word-wrap: break-word;
  114. overflow-wrap: break-word;
  115. }
  116. @media (prefers-color-scheme: dark) {
  117. #${NotificationId} {
  118. background-color: rgba(50, 50, 50, 0.85);
  119. color: #eee;
  120. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
  121. }
  122. #${NotificationId} .pr-title {
  123. color: #f0f0f0;
  124. }
  125. #${NotificationId} .pr-message {
  126. color: #ccc;
  127. }
  128. }
  129. `);
  130. }
  131.  
  132. let NotificationTimer = null;
  133. let RemovalTimer = null;
  134.  
  135. function ShowNotification(Message) {
  136. if (NotificationTimer) clearTimeout(NotificationTimer);
  137. if (RemovalTimer) clearTimeout(RemovalTimer);
  138.  
  139. const ExistingNotification = document.getElementById(NotificationId);
  140. if (ExistingNotification) {
  141. ExistingNotification.remove();
  142. }
  143.  
  144. const NotificationElement = document.createElement("div");
  145. NotificationElement.id = NotificationId;
  146.  
  147. const LocalizedTitle = GetLocalizedScriptTitle();
  148.  
  149. NotificationElement.innerHTML = `
  150. <img src="${ScriptIconUrl}" alt="Icon" class="pr-icon">
  151. <div class="pr-content">
  152. <div class="pr-title">${LocalizedTitle}</div>
  153. <div class="pr-message">${Message}</div>
  154. </div>
  155. `;
  156.  
  157. document.body.appendChild(NotificationElement);
  158.  
  159. requestAnimationFrame(() => {
  160. requestAnimationFrame(() => {
  161. NotificationElement.classList.add("visible");
  162. });
  163. });
  164.  
  165. NotificationTimer = setTimeout(() => {
  166. NotificationElement.classList.remove("visible");
  167. RemovalTimer = setTimeout(() => {
  168. if (NotificationElement.parentNode) {
  169. NotificationElement.remove();
  170. }
  171. NotificationTimer = null;
  172. RemovalTimer = null;
  173. }, AnimationDuration);
  174. }, NotificationTimeout);
  175. }
  176.  
  177. function GetLanguageKey() {
  178. const Lang = navigator.language;
  179. if (Lang.startsWith("zh")) {
  180. if (Lang === "zh-TW" || Lang === "zh-HK" || Lang === "zh-Hant") {
  181. return "zh-TW";
  182. } else {
  183. return "zh-CN";
  184. }
  185. } else {
  186. if (Lang.startsWith("en")) {
  187. return "en-US";
  188. }
  189. return "en-US";
  190. }
  191. }
  192.  
  193. function GetLocalizedScriptTitle() {
  194. const LangKey = GetLanguageKey();
  195. return ScriptTitles[LangKey] || ScriptTitles["en-US"];
  196. }
  197.  
  198. function GetLocalizedMenuCommandText() {
  199. const LangKey = GetLanguageKey();
  200. return MenuCommandTexts[LangKey] || MenuCommandTexts["en-US"];
  201. }
  202.  
  203. function GetLocalizedAlertMessage(Mode) {
  204. const LangKey = GetLanguageKey();
  205. const LangMessages = AlertMessages[LangKey] || AlertMessages["en-US"];
  206. return LangMessages[Mode] || `Mode: ${Mode}`;
  207. }
  208.  
  209. let CurrentMode = GM_getValue(ModeKey, ModeHover);
  210. const LocalizedCommandText = GetLocalizedMenuCommandText();
  211.  
  212. function ShowPasswordOnHover() {
  213. this.type = "text";
  214. }
  215.  
  216. function HidePasswordOnLeave() {
  217. this.type = "password";
  218. }
  219.  
  220. function TogglePasswordOnDoubleClick() {
  221. this.type = this.type === "password" ? "text" : "password";
  222. }
  223.  
  224. function ApplyHoverBehavior(Input) {
  225. Input.addEventListener("mouseenter", ShowPasswordOnHover);
  226. Input.addEventListener("mouseleave", HidePasswordOnLeave);
  227. Input.removeEventListener("dblclick", TogglePasswordOnDoubleClick);
  228. }
  229.  
  230. function RemoveHoverBehavior(Input) {
  231. Input.removeEventListener("mouseenter", ShowPasswordOnHover);
  232. Input.removeEventListener("mouseleave", HidePasswordOnLeave);
  233. }
  234.  
  235. function ApplyDoubleClickBehavior(Input) {
  236. Input.addEventListener("dblclick", TogglePasswordOnDoubleClick);
  237. Input.removeEventListener("mouseenter", ShowPasswordOnHover);
  238. Input.removeEventListener("mouseleave", HidePasswordOnLeave);
  239. }
  240.  
  241. function RemoveDoubleClickBehavior(Input) {
  242. Input.removeEventListener("dblclick", TogglePasswordOnDoubleClick);
  243. }
  244.  
  245. function ProcessPasswordInput(Input, Mode) {
  246. RemoveHoverBehavior(Input);
  247. RemoveDoubleClickBehavior(Input);
  248.  
  249. if (Mode === ModeHover) {
  250. ApplyHoverBehavior(Input);
  251. } else if (Mode === ModeDBClick) {
  252. ApplyDoubleClickBehavior(Input);
  253. }
  254. Input.dataset.passwordProcessed = Mode;
  255. }
  256.  
  257. function ToggleMode() {
  258. const OldMode = CurrentMode;
  259. const NewMode = OldMode === ModeHover ? ModeDBClick : ModeHover;
  260. GM_setValue(ModeKey, NewMode);
  261. CurrentMode = NewMode;
  262.  
  263. const AlertMessage = GetLocalizedAlertMessage(NewMode);
  264. ShowNotification(AlertMessage);
  265.  
  266. document.querySelectorAll('input[type="password"]').forEach((Input) => {
  267. ProcessPasswordInput(Input, NewMode);
  268. });
  269. }
  270.  
  271. InjectNotificationStyles();
  272.  
  273. document
  274. .querySelectorAll('input[type="password"]')
  275. .forEach((Input) => ProcessPasswordInput(Input, CurrentMode));
  276.  
  277. const Observer = new MutationObserver((Mutations) => {
  278. Mutations.forEach((Mutation) => {
  279. if (Mutation.addedNodes && Mutation.addedNodes.length > 0) {
  280. Mutation.addedNodes.forEach((Node) => {
  281. if (
  282. Node.nodeType === Node.ELEMENT_NODE &&
  283. Node.tagName === "INPUT" &&
  284. Node.type === "password" &&
  285. !Node.dataset.passwordProcessed
  286. ) {
  287. ProcessPasswordInput(Node, CurrentMode);
  288. } else if (
  289. Node.nodeType === Node.ELEMENT_NODE &&
  290. Node.querySelectorAll
  291. ) {
  292. const PasswordInputs = Node.querySelectorAll(
  293. 'input[type="password"]:not([data-password-processed])'
  294. );
  295. PasswordInputs.forEach((Input) =>
  296. ProcessPasswordInput(Input, CurrentMode)
  297. );
  298. }
  299. });
  300. }
  301. });
  302. });
  303.  
  304. Observer.observe(document.body, {
  305. childList: true,
  306. subtree: true,
  307. });
  308.  
  309. GM_registerMenuCommand(LocalizedCommandText, ToggleMode);
  310. })();