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 = {};