密码显示助手

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

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

  1. // ==UserScript==
  2. // @name Password Revealer
  3. // @name:zh-CN 密码显示助手
  4. // @name:zh-TW 密碼顯示助手
  5. // @description Reveal Passwords By Hovering/DoubleClicking/Always Show Select Mode Via The Tampermonkey Menu Or Shortcut Ctrl/Meta+Alt+P.
  6. // @description:zh-CN 通过鼠标悬浮/双击/始终显示来显示密码框内容 可通过脚本菜单或快捷键 Ctrl/Meta+Alt+P 选择触发方式
  7. // @description:zh-TW 透過滑鼠懸浮/雙擊/始終顯示來顯示密碼框內容 可透過腳本選單或快捷鍵 Ctrl/Meta+Alt+P 選擇觸發方式
  8. // @version 1.3.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_unregisterMenuCommand
  19. // @grant GM_addStyle
  20. // @run-at document-idle
  21. // ==/UserScript==
  22.  
  23. (function () {
  24. "use strict";
  25.  
  26. const MODE_KEY = "PasswordDisplayMode";
  27. const MODE_HOVER = "Hover";
  28. const MODE_DBLCLICK = "DoubleClick";
  29. const MODE_ALWAYS_SHOW = "AlwaysShow";
  30. const NOTIFICATION_ID = "PasswordRevealerNotification";
  31. const NOTIFICATION_TIMEOUT = 2000;
  32. const ANIMATION_DURATION = 300;
  33. const SCRIPT_ICON_URL =
  34. "https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/PasswordRevealerIcon.svg";
  35. const PROCESSED_ATTRIBUTE = "data-password-revealer-processed";
  36.  
  37. const VALID_MODES = [MODE_HOVER, MODE_DBLCLICK, MODE_ALWAYS_SHOW];
  38.  
  39. const LOCALIZATION = {
  40. "en-US": {
  41. ScriptTitle: "Password Revealer",
  42. MenuCmdSetHover: "「Hover」Mode",
  43. MenuCmdSetDBClick: "「Double Click」Mode",
  44. MenuCmdSetAlwaysShow: "「Always Show」Mode",
  45. AlertMessages: {
  46. [MODE_HOVER]: "Mode Switched To 「Hover」",
  47. [MODE_DBLCLICK]: "Mode Switched To 「Double Click」",
  48. [MODE_ALWAYS_SHOW]: "Mode Switched To 「Always Show」",
  49. },
  50. },
  51. "zh-CN": {
  52. ScriptTitle: "密码显示助手",
  53. MenuCmdSetHover: "「悬浮显示」模式",
  54. MenuCmdSetDBClick: "「双击切换」模式",
  55. MenuCmdSetAlwaysShow: "「始终显示」模式",
  56. AlertMessages: {
  57. [MODE_HOVER]: "模式已切换为「悬浮显示」",
  58. [MODE_DBLCLICK]: "模式已切换为「双击切换」",
  59. [MODE_ALWAYS_SHOW]: "模式已切换为「始终显示」",
  60. },
  61. },
  62. "zh-TW": {
  63. ScriptTitle: "密碼顯示助手",
  64. MenuCmdSetHover: "「懸浮顯示」模式",
  65. MenuCmdSetDBClick: "「雙擊切換」模式",
  66. MenuCmdSetAlwaysShow: "「始終顯示」模式",
  67. AlertMessages: {
  68. [MODE_HOVER]: "模式已切換為「懸浮顯示」",
  69. [MODE_DBLCLICK]: "模式已切換為「雙擊切換」",
  70. [MODE_ALWAYS_SHOW]: "模式已切換為「始終顯示」",
  71. },
  72. },
  73. };
  74.  
  75. const MODE_MENU_TEXT_KEYS = {
  76. [MODE_HOVER]: "MenuCmdSetHover",
  77. [MODE_DBLCLICK]: "MenuCmdSetDBClick",
  78. [MODE_ALWAYS_SHOW]: "MenuCmdSetAlwaysShow",
  79. };
  80.  
  81. let registeredMenuCommandIds = [];
  82. let notificationTimer = null;
  83. let removalTimer = null;
  84. let currentMode = GM_getValue(MODE_KEY, MODE_HOVER);
  85.  
  86. function getLanguageKey() {
  87. const lang = navigator.language;
  88. if (lang.startsWith("zh")) {
  89. return lang === "zh-TW" || lang === "zh-HK" || lang === "zh-Hant"
  90. ? "zh-TW"
  91. : "zh-CN";
  92. }
  93. return "en-US";
  94. }
  95.  
  96. function getLocalizedText(key, subKey = null, fallbackLang = "en-US") {
  97. const langKey = getLanguageKey();
  98. const primaryLangData = LOCALIZATION[langKey] || LOCALIZATION[fallbackLang];
  99. const fallbackLangData = LOCALIZATION[fallbackLang];
  100.  
  101. let value;
  102. if (subKey && key === "AlertMessages") {
  103. value = primaryLangData[key]?.[subKey] ?? fallbackLangData[key]?.[subKey];
  104. } else {
  105. value = primaryLangData[key] ?? fallbackLangData[key];
  106. }
  107. return value ?? (subKey ? `${key}.${subKey}?` : `${key}?`);
  108. }
  109.  
  110. function injectNotificationStyles() {
  111. GM_addStyle(`
  112. #${NOTIFICATION_ID} {
  113. position: fixed;
  114. top: 20px;
  115. right: -400px;
  116. width: 300px;
  117. background-color: rgba(240, 240, 240, 0.9);
  118. color: #333;
  119. padding: 10px;
  120. border-radius: 10px;
  121. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  122. z-index: 99999;
  123. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
  124. display: flex;
  125. align-items: flex-start;
  126. opacity: 0;
  127. transition: right ${ANIMATION_DURATION}ms ease-out, opacity ${
  128. ANIMATION_DURATION * 0.8
  129. }ms ease-out;
  130. box-sizing: border-box;
  131. backdrop-filter: blur(8px);
  132. -webkit-backdrop-filter: blur(8px);
  133. }
  134.  
  135. #${NOTIFICATION_ID}.visible {
  136. right: 20px;
  137. opacity: 1;
  138. }
  139.  
  140. #${NOTIFICATION_ID} .pr-icon {
  141. width: 32px;
  142. height: 32px;
  143. margin-right: 10px;
  144. flex-shrink: 0;
  145. }
  146.  
  147. #${NOTIFICATION_ID} .pr-content {
  148. display: flex;
  149. flex-direction: column;
  150. flex-grow: 1;
  151. min-width: 0;
  152. }
  153.  
  154. #${NOTIFICATION_ID} .pr-title {
  155. font-size: 13px;
  156. font-weight: 600;
  157. margin-bottom: 2px;
  158. color: #111;
  159. white-space: nowrap;
  160. overflow: hidden;
  161. text-overflow: ellipsis;
  162. }
  163.  
  164. #${NOTIFICATION_ID} .pr-message {
  165. font-size: 12px;
  166. line-height: 1.3;
  167. color: #444;
  168. word-wrap: break-word;
  169. overflow-wrap: break-word;
  170. }
  171.  
  172. @media (prefers-color-scheme: dark) {
  173. #${NOTIFICATION_ID} {
  174. background-color: rgba(50, 50, 50, 0.85);
  175. color: #eee;
  176. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
  177. }
  178. #${NOTIFICATION_ID} .pr-title {
  179. color: #f0f0f0;
  180. }
  181. #${NOTIFICATION_ID} .pr-message {
  182. color: #ccc;
  183. }
  184. }
  185. `);
  186. }
  187.  
  188. function showNotification(message) {
  189. if (notificationTimer) clearTimeout(notificationTimer);
  190. if (removalTimer) clearTimeout(removalTimer);
  191.  
  192. const existingNotification = document.getElementById(NOTIFICATION_ID);
  193. if (existingNotification) {
  194. existingNotification.remove();
  195. }
  196.  
  197. const notificationElement = document.createElement("div");
  198. notificationElement.id = NOTIFICATION_ID;
  199. notificationElement.innerHTML = `
  200. <img src="${SCRIPT_ICON_URL}" alt="" class="pr-icon">
  201. <div class="pr-content">
  202. <div class="pr-title">${getLocalizedText("ScriptTitle")}</div>
  203. <div class="pr-message">${message}</div>
  204. </div>
  205. `;
  206.  
  207. document.body.appendChild(notificationElement);
  208.  
  209. requestAnimationFrame(() => {
  210. notificationElement.classList.add("visible");
  211. });
  212.  
  213. notificationTimer = setTimeout(() => {
  214. notificationElement.classList.remove("visible");
  215. removalTimer = setTimeout(() => {
  216. if (notificationElement.parentNode) {
  217. notificationElement.remove();
  218. }
  219. notificationTimer = null;
  220. removalTimer = null;
  221. }, ANIMATION_DURATION);
  222. }, NOTIFICATION_TIMEOUT);
  223. }
  224.  
  225. function processPasswordInput(input, mode) {
  226. if (
  227. !(input instanceof HTMLInputElement) ||
  228. input.type === "hidden" ||
  229. input.hasAttribute(PROCESSED_ATTRIBUTE)
  230. ) {
  231. return;
  232. }
  233.  
  234. if (mode === MODE_ALWAYS_SHOW) {
  235. input.type = "text";
  236. } else {
  237. if (input.type !== "password") {
  238. input.type = "password";
  239. }
  240. }
  241. input.setAttribute(PROCESSED_ATTRIBUTE, mode);
  242. }
  243.  
  244. function findAllPasswordInputs(rootNode) {
  245. const results = [];
  246. try {
  247. rootNode
  248. .querySelectorAll(
  249. `input[type="password"]:not([${PROCESSED_ATTRIBUTE}])`
  250. )
  251. .forEach((input) => results.push(input));
  252.  
  253. rootNode.querySelectorAll("*").forEach((el) => {
  254. if (el.shadowRoot) {
  255. results.push(...findAllPasswordInputs(el.shadowRoot));
  256. }
  257. });
  258. } catch (e) {}
  259. return results;
  260. }
  261.  
  262. function setMode(newMode) {
  263. if (currentMode === newMode || !VALID_MODES.includes(newMode)) {
  264. return;
  265. }
  266.  
  267. const oldMode = currentMode;
  268. currentMode = newMode;
  269. GM_setValue(MODE_KEY, currentMode);
  270.  
  271. const alertMessage = getLocalizedText("AlertMessages", currentMode);
  272. showNotification(alertMessage);
  273.  
  274. const processedInputs = document.querySelectorAll(
  275. `input[${PROCESSED_ATTRIBUTE}]`
  276. );
  277. processedInputs.forEach((input) => {
  278. input.setAttribute(PROCESSED_ATTRIBUTE, currentMode);
  279. if (currentMode === MODE_ALWAYS_SHOW) {
  280. input.type = "text";
  281. } else if (oldMode === MODE_ALWAYS_SHOW) {
  282. input.type = "password";
  283. }
  284. });
  285.  
  286. const untrackedPasswordInputs = findAllPasswordInputs(document.body);
  287. untrackedPasswordInputs.forEach((input) =>
  288. processPasswordInput(input, currentMode)
  289. );
  290.  
  291. registerModeMenuCommands();
  292. }
  293.  
  294. function registerModeMenuCommands() {
  295. registeredMenuCommandIds.forEach((id) => {
  296. try {
  297. GM_unregisterMenuCommand(id);
  298. } catch (e) {}
  299. });
  300. registeredMenuCommandIds = [];
  301.  
  302. VALID_MODES.forEach((mode) => {
  303. const menuKey = MODE_MENU_TEXT_KEYS[mode];
  304. const baseText = getLocalizedText(menuKey);
  305. const commandText = baseText + (mode === currentMode ? " ✅" : "");
  306.  
  307. const commandId = GM_registerMenuCommand(commandText, () =>
  308. setMode(mode)
  309. );
  310. registeredMenuCommandIds.push(commandId);
  311. });
  312. }
  313.  
  314. function showPasswordOnHover(event) {
  315. const input = event.target;
  316. if (
  317. currentMode === MODE_HOVER &&
  318. input.matches(
  319. `input[type="password"][${PROCESSED_ATTRIBUTE}="${MODE_HOVER}"]`
  320. )
  321. ) {
  322. input.type = "text";
  323. }
  324. }
  325.  
  326. function hidePasswordOnLeave(event) {
  327. const input = event.target;
  328. if (
  329. currentMode === MODE_HOVER &&
  330. input.matches(
  331. `input[type="text"][${PROCESSED_ATTRIBUTE}="${MODE_HOVER}"]`
  332. )
  333. ) {
  334. input.type = "password";
  335. }
  336. }
  337.  
  338. function togglePasswordOnDoubleClick(event) {
  339. const input = event.target;
  340. if (
  341. currentMode === MODE_DBLCLICK &&
  342. input.matches(`input[${PROCESSED_ATTRIBUTE}="${MODE_DBLCLICK}"]`)
  343. ) {
  344. input.type = input.type === "password" ? "text" : "password";
  345. }
  346. }
  347.  
  348. function initializeEventListeners() {
  349. document.body.addEventListener("mouseenter", showPasswordOnHover, true);
  350. document.body.addEventListener("mouseleave", hidePasswordOnLeave, true);
  351. document.body.addEventListener("dblclick", togglePasswordOnDoubleClick);
  352. }
  353.  
  354. function handleKeyDown(event) {
  355. if (
  356. (event.ctrlKey || event.metaKey) &&
  357. event.altKey &&
  358. event.code === "KeyP"
  359. ) {
  360. event.preventDefault();
  361. event.stopPropagation();
  362.  
  363. const currentIndex = VALID_MODES.indexOf(currentMode);
  364. const nextIndex = (currentIndex + 1) % VALID_MODES.length;
  365. const nextMode = VALID_MODES[nextIndex];
  366.  
  367. setMode(nextMode);
  368. }
  369. }
  370.  
  371. const observerCallback = (mutationsList) => {
  372. for (const mutation of mutationsList) {
  373. if (mutation.type === "childList") {
  374. mutation.addedNodes.forEach((node) => {
  375. if (node.nodeType === Node.ELEMENT_NODE) {
  376. const inputsToProcess = findAllPasswordInputs(node);
  377. inputsToProcess.forEach((input) =>
  378. processPasswordInput(input, currentMode)
  379. );
  380. }
  381. });
  382. } else if (
  383. mutation.type === "attributes" &&
  384. mutation.attributeName === "type"
  385. ) {
  386. const targetInput = mutation.target;
  387. if (
  388. targetInput.nodeType === Node.ELEMENT_NODE &&
  389. targetInput.matches &&
  390. targetInput.matches('input[type="password"]') &&
  391. !targetInput.hasAttribute(PROCESSED_ATTRIBUTE)
  392. ) {
  393. processPasswordInput(targetInput, currentMode);
  394. }
  395. }
  396. }
  397. };
  398.  
  399. const observer = new MutationObserver(observerCallback);
  400.  
  401. if (!VALID_MODES.includes(currentMode)) {
  402. currentMode = MODE_HOVER;
  403. GM_setValue(MODE_KEY, currentMode);
  404. }
  405.  
  406. injectNotificationStyles();
  407. findAllPasswordInputs(document.body).forEach((input) =>
  408. processPasswordInput(input, currentMode)
  409. );
  410.  
  411. initializeEventListeners();
  412. document.addEventListener("keydown", handleKeyDown, true);
  413.  
  414. observer.observe(document.body, {
  415. childList: true,
  416. subtree: true,
  417. attributes: true,
  418. attributeFilter: ["type"],
  419. });
  420.  
  421. registerModeMenuCommands();
  422. })();