From c9cbcd44cc78823ce5f6378da0918f5b7f8b8df1 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 20 May 2026 06:06:38 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9A=90=E8=97=8F=E5=BD=95=E5=B1=8F=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E9=85=8D=E7=BD=AE=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepanel/form-dialog.js | 7 +- sidepanel/paypal-manager.js | 3 + sidepanel/sidepanel.css | 24 ++- sidepanel/sidepanel.js | 171 +++++++++++++++++++- tests/sidepanel-password-visibility.test.js | 42 ++++- tests/sidepanel-paypal-manager.test.js | 8 + 6 files changed, 249 insertions(+), 6 deletions(-) diff --git a/sidepanel/form-dialog.js b/sidepanel/form-dialog.js index 0e44133..7ea0da4 100644 --- a/sidepanel/form-dialog.js +++ b/sidepanel/form-dialog.js @@ -92,9 +92,10 @@ input.appendChild(optionNode); }); } else { + const shouldMaskInput = field.type === 'password' || field.masked === true; input = documentRef.createElement('input'); - input.type = field.type === 'password' ? 'password' : 'text'; - input.className = field.type === 'password' + input.type = shouldMaskInput ? 'password' : 'text'; + input.className = shouldMaskInput ? 'data-input data-input-with-icon' : 'data-input'; } @@ -120,7 +121,7 @@ input.dataset.fieldKey = String(field.key || ''); label.htmlFor = field.key; input.id = field.key; - if (field.type === 'password') { + if (field.type === 'password' || field.masked === true) { const inputShell = documentRef.createElement('div'); inputShell.className = 'input-with-icon'; inputShell.appendChild(input); diff --git a/sidepanel/paypal-manager.js b/sidepanel/paypal-manager.js index f4de541..6657103 100644 --- a/sidepanel/paypal-manager.js +++ b/sidepanel/paypal-manager.js @@ -212,6 +212,9 @@ key: 'email', label: 'PayPal 账号', type: 'text', + masked: true, + showPasswordLabel: '显示 PayPal 账号', + hidePasswordLabel: '隐藏 PayPal 账号', placeholder: '请输入 PayPal 登录邮箱', autocomplete: 'username', required: true, diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 1a49181..9853aee 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -658,7 +658,7 @@ header { align-items: center; min-height: 34px; margin-bottom: 14px; - padding: 7px 46px; + padding: 7px 90px; border: 1px solid color-mix(in srgb, var(--amber) 24%, var(--border)); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--amber) 10%, var(--bg-base)); @@ -675,6 +675,7 @@ header { left: 10px; top: 50%; transform: translateY(-50%); + min-width: 62px; padding: 3px 7px; border-radius: 999px; background: color-mix(in srgb, var(--amber) 22%, var(--bg-base)); @@ -682,6 +683,7 @@ header { font-size: 10px; font-weight: 700; letter-spacing: 0.08em; + text-align: center; } .auto-run-ad-badge--end { @@ -1381,6 +1383,26 @@ header { .data-textarea::placeholder { color: var(--text-muted); } .data-textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); } +.textarea-with-icon { + position: relative; + flex: 1; + min-width: 0; + width: 100%; + display: flex; +} + +.data-textarea-with-icon { + padding-right: 38px; +} + +.data-textarea.is-privacy-masked { + -webkit-text-security: disc; +} + +.textarea-with-icon .input-icon-btn { + top: 8px; +} + .data-input-with-icon { padding-right: 38px; } diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 10bcb4d..eec0060 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -1550,6 +1550,50 @@ let customEmailPoolEntriesState = []; const EYE_OPEN_ICON = ''; const EYE_CLOSED_ICON = ''; +const PRIVACY_MASKED_INPUT_IDS = Object.freeze([ + 'input-contribution-qq', + 'input-sub2api-url', + 'input-sub2api-email', + 'input-sub2api-default-proxy', + 'input-codex2api-url', + 'input-kiro-rs-url', + 'input-gpc-helper-api', + 'input-gpc-helper-phone', + 'input-gpc-helper-local-sms-url', + 'input-gopay-phone', + 'input-gopay-otp', + 'input-email-prefix', + 'input-inbucket-host', + 'input-inbucket-mailbox', + 'input-email', + 'input-temp-email-base-url', + 'input-temp-email-receive-mailbox', + 'input-cloud-mail-base-url', + 'input-cloud-mail-admin-email', + 'input-cloud-mail-receive-mailbox', + 'input-cloud-mail-domain', + 'input-yyds-mail-base-url', + 'input-hotmail-remote-base-url', + 'input-hotmail-local-base-url', + 'input-hotmail-email', + 'input-hotmail-client-id', + 'input-mail2925-email', + 'input-luckmail-base-url', + 'input-luckmail-domain', + 'input-ip-proxy-account-session-prefix', + 'input-ip-proxy-host', + 'input-ip-proxy-region', + 'input-account-run-history-helper-base-url', + 'input-free-reusable-phone', + 'input-signup-phone', +]); +const PRIVACY_MASKED_TEXTAREA_IDS = Object.freeze([ + 'input-custom-mail-provider-pool', + 'input-custom-email-pool-import', + 'input-hotmail-import', + 'input-mail2925-import', + 'input-ip-proxy-account-list', +]); const COPY_ICON = ''; const parseHotmailImportText = window.HotmailUtils?.parseHotmailImportText; const normalizeHotmailServiceModeFromUtils = window.HotmailUtils?.normalizeHotmailServiceMode; @@ -13244,6 +13288,132 @@ function bindPasswordVisibilityToggles(root = document) { }); } +function getPrivacyMaskFieldLabel(control) { + const row = control?.closest?.('.data-row, .modal-form-row'); + const label = row?.querySelector?.('.data-label, .modal-form-label')?.textContent; + const fallback = control?.getAttribute?.('aria-label') + || control?.placeholder + || control?.id + || '\u5185\u5bb9'; + return String(label || fallback || '\u5185\u5bb9').replace(/\s+/g, ' ').trim(); +} + +function getPrivacyMaskLabels(control) { + const label = getPrivacyMaskFieldLabel(control) || '\u5185\u5bb9'; + return { + show: `\u663e\u793a${label}`, + hide: `\u9690\u85cf${label}`, + }; +} + +function ensurePrivacyControlShell(control, className) { + const parent = control?.parentElement || null; + if (!control || !parent) { + return null; + } + if (parent.classList?.contains(className)) { + return parent; + } + const shell = document.createElement('div'); + shell.className = className; + parent.insertBefore(shell, control); + shell.appendChild(control); + return shell; +} + +function createPrivacyToggleButton(control, labels) { + const button = document.createElement('button'); + button.className = 'input-icon-btn'; + button.type = 'button'; + if (control?.id) { + button.id = `btn-toggle-${control.id.replace(/^input-/, '')}`; + } + button.setAttribute('aria-label', labels.show); + button.title = labels.show; + return button; +} + +function installPrivacyMaskedInput(input) { + if (!input || input.type === 'hidden') { + return; + } + const labels = getPrivacyMaskLabels(input); + input.type = 'password'; + input.classList?.add('data-input-with-icon'); + const shell = ensurePrivacyControlShell(input, 'input-with-icon'); + if (!shell) { + return; + } + + let button = shell.querySelector?.(`[data-password-toggle="${input.id}"]`) || null; + if (!button) { + button = createPrivacyToggleButton(input, labels); + shell.appendChild(button); + } + button.dataset.passwordToggle = input.id; + button.dataset.showLabel = button.dataset.showLabel || labels.show; + button.dataset.hideLabel = button.dataset.hideLabel || labels.hide; + syncPasswordVisibilityToggle(button); +} + +function syncPrivacyTextareaToggle(button, textarea) { + if (!button || !textarea) { + return; + } + const labels = getPrivacyMaskLabels(textarea); + const isHidden = textarea.dataset?.privacyMasked !== 'false'; + textarea.classList?.toggle('is-privacy-masked', isHidden); + button.innerHTML = isHidden ? EYE_OPEN_ICON : EYE_CLOSED_ICON; + button.setAttribute('aria-label', isHidden ? labels.show : labels.hide); + button.title = isHidden ? labels.show : labels.hide; +} + +function installPrivacyMaskedTextarea(textarea) { + if (!textarea) { + return; + } + const labels = getPrivacyMaskLabels(textarea); + textarea.classList?.add('data-textarea-with-icon', 'is-privacy-masked'); + if (!textarea.dataset) { + return; + } + textarea.dataset.privacyMasked = textarea.dataset.privacyMasked || 'true'; + const shell = ensurePrivacyControlShell(textarea, 'textarea-with-icon'); + if (!shell) { + return; + } + + let button = shell.querySelector?.(`[data-privacy-textarea-toggle="${textarea.id}"]`) || null; + if (!button) { + button = createPrivacyToggleButton(textarea, labels); + shell.appendChild(button); + } + button.dataset.privacyTextareaToggle = textarea.id; + syncPrivacyTextareaToggle(button, textarea); + if (button.dataset?.privacyTextareaToggleBound === 'true') { + return; + } + button.dataset.privacyTextareaToggleBound = 'true'; + button.addEventListener('click', () => { + textarea.dataset.privacyMasked = textarea.dataset.privacyMasked === 'false' ? 'true' : 'false'; + syncPrivacyTextareaToggle(button, textarea); + }); +} + +function installPrivacyMaskControls(root = document) { + PRIVACY_MASKED_INPUT_IDS.forEach((inputId) => { + const input = root.getElementById?.(inputId) || document.getElementById(inputId); + installPrivacyMaskedInput(input); + }); + PRIVACY_MASKED_TEXTAREA_IDS.forEach((textareaId) => { + const textarea = root.getElementById?.(textareaId) || document.getElementById(textareaId); + installPrivacyMaskedTextarea(textarea); + }); +} + +installPrivacyMaskControls(); +bindPasswordVisibilityToggles(); + async function copyTextToClipboard(text) { const value = String(text || '').trim(); if (!value) { @@ -17437,7 +17607,6 @@ document.addEventListener('scroll', () => { // ============================================================ initializeManualStepActions(); -bindPasswordVisibilityToggles(); initTheme(); initHotmailListExpandedState(); initMail2925ListExpandedState(); diff --git a/tests/sidepanel-password-visibility.test.js b/tests/sidepanel-password-visibility.test.js index 8bc2f25..bfeadbf 100644 --- a/tests/sidepanel-password-visibility.test.js +++ b/tests/sidepanel-password-visibility.test.js @@ -33,7 +33,47 @@ test('sidepanel password inputs expose visibility toggles', () => { test('shared form dialog adds visibility toggles for password fields', () => { const source = fs.readFileSync('sidepanel/form-dialog.js', 'utf8'); - assert.match(source, /field\.type === 'password'[\s\S]*data-input-with-icon/); + assert.match(source, /field\.type === 'password' \|\| field\.masked === true/); + assert.match(source, /shouldMaskInput[\s\S]*data-input-with-icon/); assert.match(source, /syncPasswordToggleButton\(toggleButton,\s*input,\s*labels\)/); assert.match(source, /input\.type = input\.type === 'password' \? 'text' : 'password'/); }); + +test('sidepanel masks video-sensitive settings with reusable visibility controls', () => { + const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8'); + + [ + 'input-sub2api-url', + 'input-sub2api-email', + 'input-codex2api-url', + 'input-kiro-rs-url', + 'input-email', + 'input-hotmail-email', + 'input-mail2925-email', + 'input-ip-proxy-host', + 'input-signup-phone', + ].forEach((inputId) => { + assert.match(source, new RegExp(`'${inputId}'`)); + }); + + assert.match(source, /function installPrivacyMaskControls/); + assert.match(source, /installPrivacyMaskControls\(\);\s*bindPasswordVisibilityToggles\(\);/); +}); + +test('sidepanel masks bulk text areas with an eye toggle', () => { + const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8'); + const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8'); + + [ + 'input-custom-mail-provider-pool', + 'input-custom-email-pool-import', + 'input-hotmail-import', + 'input-mail2925-import', + 'input-ip-proxy-account-list', + ].forEach((textareaId) => { + assert.match(source, new RegExp(`'${textareaId}'`)); + }); + + assert.match(source, /data-privacy-textarea-toggle/); + assert.match(css, /-webkit-text-security:\s*disc/); +}); diff --git a/tests/sidepanel-paypal-manager.test.js b/tests/sidepanel-paypal-manager.test.js index 683caca..4223547 100644 --- a/tests/sidepanel-paypal-manager.test.js +++ b/tests/sidepanel-paypal-manager.test.js @@ -44,6 +44,14 @@ test('sidepanel html contains paypal select and GoPay controls', () => { assert.match(html, /id="shared-form-modal"/); }); +test('paypal account dialog masks the login account field for recording', () => { + const source = fs.readFileSync('sidepanel/paypal-manager.js', 'utf8'); + + assert.match(source, /label:\s*'PayPal 账号'[\s\S]*masked:\s*true/); + assert.match(source, /showPasswordLabel:\s*'显示 PayPal 账号'/); + assert.match(source, /hidePasswordLabel:\s*'隐藏 PayPal 账号'/); +}); + test('paypal manager saves a paypal account and selects it immediately', async () => { const source = fs.readFileSync('sidepanel/paypal-manager.js', 'utf8'); const windowObject = {};