revert: restore entire src/ from pre-doc-scanner commit (e4b44a7)
Going back to a known-good baseline. This version had: - Working reveal mode with CSS Highlight API - Working substitution (fetch + XHR hooks) - Smart patterns (names, emails, phones, usernames) - Encryption/sync (password, TOTP, WebAuthn) - Multiple identity profiles - Activity log - Secret scanner - Auto-detect PII warnings - Pre-send PII detection Kept current manifests (UUID, data_collection_permissions, version). No renames applied — uses original naming (secretScanning, PPI, etc). Will re-apply renames and new features from this working base. https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
@@ -623,35 +623,6 @@ body {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Options tab */
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.setting-item:last-of-type { border-bottom: none; }
|
||||
|
||||
.setting-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setting-label strong {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.setting-label .setting-desc {
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 8px 16px;
|
||||
|
||||
+4
-77
@@ -33,7 +33,6 @@
|
||||
<button class="tab" data-tab="mappings">Mappings</button>
|
||||
<button class="tab" data-tab="activity">Activity</button>
|
||||
<button class="tab" data-tab="test">Test</button>
|
||||
<button class="tab" data-tab="options">Options</button>
|
||||
</nav>
|
||||
|
||||
<!-- Locked State Overlay -->
|
||||
@@ -194,75 +193,6 @@
|
||||
<div class="test-identity-status" id="identityStatus"></div>
|
||||
</section>
|
||||
|
||||
<!-- Options Tab -->
|
||||
<section class="tab-content" id="tab-options">
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Auto Redact</strong>
|
||||
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards, and custom patterns</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Auto-detect PII</strong>
|
||||
<span class="setting-desc">Warn about unconfigured personal data (IPs, addresses, paths)</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optAutoDetect" checked><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Auto-redact detected PII</strong>
|
||||
<span class="setting-desc">Replace detected PII with placeholders on send</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optAutoRedactDetected" checked><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Show highlights</strong>
|
||||
<span class="setting-desc">Highlight substituted values in AI responses</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optHighlights"><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Document scan preview</strong>
|
||||
<span class="setting-desc">Show PII findings before uploading documents</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">
|
||||
<strong>Detect proper nouns</strong>
|
||||
<span class="setting-desc">Flag capitalized phrases (names, companies) — may produce false positives</span>
|
||||
</div>
|
||||
<label class="toggle"><input type="checkbox" id="optProperNouns"><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e5e7eb">
|
||||
<div class="setting-label" style="margin-bottom:6px">
|
||||
<strong>Custom Domains</strong>
|
||||
<span class="setting-desc">Add sites beyond the built-in list</span>
|
||||
</div>
|
||||
<div id="popupDomainList" style="max-height:120px;overflow-y:auto;margin-bottom:6px"></div>
|
||||
<div style="display:flex;gap:4px">
|
||||
<input type="text" id="popupNewDomain" placeholder="https://ai.example.com" style="flex:1;font-size:11px;padding:4px 6px;border:1px solid #d1d5db;border-radius:4px;min-width:0">
|
||||
<button class="btn" id="btnPopupAddDomain" style="font-size:11px;padding:4px 8px;white-space:nowrap">Add</button>
|
||||
</div>
|
||||
<div id="popupDomainSuggestions" style="margin-top:6px;display:flex;flex-wrap:wrap;gap:3px"></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e5e7eb">
|
||||
<button class="btn" id="btnOpenFullOptions" style="width:100%;font-size:12px">Open Full Options Page</button>
|
||||
<p class="help-text" style="margin-top:6px;text-align:center">Sync, encryption, org, version history, import/export, and more</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="privacy-note">
|
||||
@@ -271,14 +201,11 @@
|
||||
AES-256 encrypted at rest — unreadable without your password.</span>
|
||||
</div>
|
||||
<div class="privacy-note" style="color:#b45309;background:#fef3c7;padding:6px 8px;border-radius:4px;margin-bottom:6px">
|
||||
Silent Send is a convenience tool, not a security guarantee. It reduces
|
||||
but cannot eliminate the risk of sharing personal data. It may miss PII
|
||||
in images, unusual formats, or data you haven't configured. Third-party
|
||||
sites may change how they send data at any time, which can cause missed
|
||||
substitutions without warning. Always verify sensitive messages before
|
||||
sending. By using this extension, you accept full responsibility for
|
||||
verifying your data is protected.
|
||||
Silent Send is a convenience tool, not a security guarantee. It can miss
|
||||
PPI in images, file uploads, unusual name forms, or data you forgot to
|
||||
configure. Always verify sensitive messages before sending.
|
||||
</div>
|
||||
<a href="#" id="btnOptions">Options</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
+41
-196
@@ -1,6 +1,6 @@
|
||||
import SubstitutionEngine from '../lib/substitution-engine.js';
|
||||
import SmartPatterns from '../lib/smart-patterns.js';
|
||||
import AutoRedact from '../lib/auto-redact.js';
|
||||
import SecretScanner from '../lib/secret-scanner.js';
|
||||
import AutoDetect from '../lib/auto-detect.js';
|
||||
import Storage from '../lib/storage.js';
|
||||
import SilentSendSync from '../lib/sync.js';
|
||||
@@ -18,12 +18,6 @@ let settings = {};
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
const $$ = (sel) => document.querySelectorAll(sel);
|
||||
|
||||
// --- Safe innerHTML replacement (AMO-compliant) ---
|
||||
function safeHTML(el, html) {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
el.replaceChildren(...Array.from(doc.body.childNodes));
|
||||
}
|
||||
|
||||
// --- Init ---
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Check if locked BEFORE trying to read sensitive data
|
||||
@@ -208,45 +202,12 @@ async function initUnlockedUI() {
|
||||
});
|
||||
});
|
||||
|
||||
// Open full options page button (Options tab)
|
||||
$('#btnOpenFullOptions').addEventListener('click', () => {
|
||||
// Options link
|
||||
$('#btnOptions').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
api.runtime.openOptionsPage();
|
||||
});
|
||||
|
||||
// Load options tab settings
|
||||
$('#optAutoRedact').checked = settings.autoRedact !== false;
|
||||
$('#optAutoDetect').checked = settings.autoDetect !== false;
|
||||
$('#optAutoRedactDetected').checked = settings.autoRedactDetected !== false;
|
||||
$('#optHighlights').checked = settings.showHighlights || false;
|
||||
$('#optDocPreview').checked = settings.docScanPreview !== false;
|
||||
$('#optProperNouns').checked = settings.detectProperNouns || false;
|
||||
|
||||
// Options tab change handlers
|
||||
const optHandlers = [
|
||||
['optAutoRedact', 'autoRedact'],
|
||||
['optAutoDetect', 'autoDetect'],
|
||||
['optAutoRedactDetected', 'autoRedactDetected'],
|
||||
['optHighlights', 'showHighlights'],
|
||||
['optDocPreview', 'docScanPreview'],
|
||||
['optProperNouns', 'detectProperNouns'],
|
||||
];
|
||||
for (const [id, key] of optHandlers) {
|
||||
$(`#${id}`).addEventListener('change', async (e) => {
|
||||
settings[key] = e.target.checked;
|
||||
await Storage.saveSettings({ [key]: e.target.checked });
|
||||
api.runtime.sendMessage({ type: 'update:settings', settings });
|
||||
});
|
||||
}
|
||||
|
||||
// --- Popup domain management ---
|
||||
renderPopupDomains();
|
||||
renderPopupDomainSuggestions();
|
||||
|
||||
$('#btnPopupAddDomain').addEventListener('click', popupAddDomain);
|
||||
$('#popupNewDomain').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') popupAddDomain();
|
||||
});
|
||||
|
||||
// Update privacy note based on encryption state
|
||||
const encEnabled = await Storage._isAtRestEncryptionEnabled();
|
||||
const encNote = $('#privacyEncNote');
|
||||
@@ -341,17 +302,11 @@ async function showLockedUI() {
|
||||
// --- Profiles ---
|
||||
function renderProfileSelector() {
|
||||
const select = $('#profileSelect');
|
||||
// Build options via DOM API — safeHTML + DOMParser mangles <option> elements
|
||||
select.replaceChildren();
|
||||
for (const p of profiles) {
|
||||
const opt = new Option(
|
||||
`${p.name}${p.active ? '' : ' (off)'}`,
|
||||
p.id,
|
||||
false,
|
||||
p.id === currentProfileId
|
||||
);
|
||||
select.appendChild(opt);
|
||||
}
|
||||
select.innerHTML = profiles.map(p =>
|
||||
`<option value="${p.id}" ${p.id === currentProfileId ? 'selected' : ''}>` +
|
||||
`${escapeHtml(p.name)}${p.active ? '' : ' (off)'}` +
|
||||
`</option>`
|
||||
).join('');
|
||||
|
||||
const profile = profiles.find(p => p.id === currentProfileId);
|
||||
$('#profileActive').checked = profile?.active ?? true;
|
||||
@@ -400,7 +355,7 @@ function renderFieldList(fieldName, items) {
|
||||
items = [{ real: '', substitute: '', type: config.defaultType || '' }];
|
||||
}
|
||||
|
||||
safeHTML(container, items.map((item, i) => {
|
||||
container.innerHTML = items.map((item, i) => {
|
||||
let typeHtml = '';
|
||||
if (config.typeOptions) {
|
||||
typeHtml = `<select class="id-type-select" data-index="${i}" style="padding:3px 2px;font-size:10px;border:1px solid #e5e7eb;border-radius:3px;width:42px">` +
|
||||
@@ -416,7 +371,7 @@ function renderFieldList(fieldName, items) {
|
||||
<input type="text" class="input input-sm id-sub" value="${escapeAttr(item.substitute || '')}" placeholder="${config.placeholderSub}">
|
||||
<button class="btn-remove" title="Remove">×</button>
|
||||
</div>`;
|
||||
}).join(''));
|
||||
}).join('');
|
||||
|
||||
// Bind remove buttons
|
||||
container.querySelectorAll('.btn-remove').forEach(btn => {
|
||||
@@ -465,13 +420,13 @@ function loadIdentityForm() {
|
||||
).join('') +
|
||||
`</select>`;
|
||||
}
|
||||
safeHTML(tempDiv, `<div class="id-entry-row" data-index="${count}">
|
||||
tempDiv.innerHTML = `<div class="id-entry-row" data-index="${count}">
|
||||
${typeHtml}
|
||||
<input type="text" class="input input-sm id-real" placeholder="${config.placeholderReal}">
|
||||
<span class="arrow" style="font-size:12px">→</span>
|
||||
<input type="text" class="input input-sm id-sub" placeholder="${config.placeholderSub}">
|
||||
<button class="btn-remove" title="Remove">×</button>
|
||||
</div>`);
|
||||
</div>`;
|
||||
const row = tempDiv.firstElementChild;
|
||||
container.appendChild(row);
|
||||
row.querySelector('.btn-remove').addEventListener('click', () => {
|
||||
@@ -621,11 +576,11 @@ function renderMappings() {
|
||||
const list = $('#mappingList');
|
||||
|
||||
if (mappings.length === 0) {
|
||||
safeHTML(list, '<div class="empty-state">No mappings yet. Add your first one above.</div>');
|
||||
list.innerHTML = '<div class="empty-state">No mappings yet. Add your first one above.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
safeHTML(list, mappings
|
||||
list.innerHTML = mappings
|
||||
.map(
|
||||
(m) => `
|
||||
<div class="mapping-item" data-id="${m.id}">
|
||||
@@ -642,7 +597,7 @@ function renderMappings() {
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join(''));
|
||||
.join('');
|
||||
|
||||
// Bind actions
|
||||
list.querySelectorAll('.btn-delete').forEach((btn) => {
|
||||
@@ -676,11 +631,11 @@ async function renderActivity() {
|
||||
countEl.textContent = `${log.length} substitution${log.length !== 1 ? 's' : ''} logged`;
|
||||
|
||||
if (log.length === 0) {
|
||||
safeHTML(list, '<div class="empty-state">No activity yet.</div>');
|
||||
list.innerHTML = '<div class="empty-state">No activity yet.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
safeHTML(list, log
|
||||
list.innerHTML = log
|
||||
.slice(0, 50)
|
||||
.map((entry) => {
|
||||
const time = new Date(entry.timestamp).toLocaleTimeString([], {
|
||||
@@ -698,7 +653,7 @@ async function renderActivity() {
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join(''));
|
||||
.join('');
|
||||
}
|
||||
|
||||
// --- Test Diff (Strip: real → fake) ---
|
||||
@@ -708,23 +663,23 @@ function renderTestDiff() {
|
||||
const stats = $('#diffStats');
|
||||
|
||||
if (!input) {
|
||||
output.replaceChildren();
|
||||
output.innerHTML = '';
|
||||
stats.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const smartResult = SmartPatterns.substitute(input, identity);
|
||||
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||
const redactResult = AutoRedact.redact(explicitResult.text, settings.customRedactPatterns);
|
||||
const secretResult = SecretScanner.redact(explicitResult.text);
|
||||
|
||||
const allReplacements = [
|
||||
...smartResult.replacements,
|
||||
...explicitResult.replacements,
|
||||
...redactResult.redactions,
|
||||
...secretResult.redactions,
|
||||
];
|
||||
const finalText = redactResult.text;
|
||||
const finalText = secretResult.text;
|
||||
|
||||
if (finalText === input && redactResult.warnings.length === 0) {
|
||||
if (finalText === input && secretResult.warnings.length === 0) {
|
||||
output.textContent = input;
|
||||
stats.textContent = 'No substitutions detected';
|
||||
return;
|
||||
@@ -740,40 +695,38 @@ function renderTestDiff() {
|
||||
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
|
||||
);
|
||||
}
|
||||
// Highlight auto-redactions in red
|
||||
for (const r of redactResult.redactions) {
|
||||
// Highlight secret redactions in red
|
||||
for (const r of secretResult.redactions) {
|
||||
const escapedReplaced = escapeHtml(r.replaced);
|
||||
html = html.replace(
|
||||
escapedReplaced,
|
||||
`<span class="sub-highlight" style="background:#fee2e2;color:#dc2626" title="${escapeHtml(r.pattern)}">${escapedReplaced}</span>`
|
||||
);
|
||||
}
|
||||
safeHTML(output, html);
|
||||
output.innerHTML = html;
|
||||
|
||||
const smartCount = smartResult.replacements.length;
|
||||
const explicitCount = explicitResult.replacements.length;
|
||||
const redactCount = redactResult.redactions.length;
|
||||
const warnCount = redactResult.warnings.length;
|
||||
const secretCount = secretResult.redactions.length;
|
||||
const warnCount = secretResult.warnings.length;
|
||||
const parts = [];
|
||||
if (smartCount > 0) parts.push(`${smartCount} smart`);
|
||||
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
||||
if (redactCount > 0) parts.push(`${redactCount} auto-redacted`);
|
||||
if (secretCount > 0) parts.push(`${secretCount} secrets redacted`);
|
||||
if (warnCount > 0) parts.push(`${warnCount} warnings`);
|
||||
|
||||
// Auto-detect unconfigured PII in the final text
|
||||
const piiWarnings = AutoDetect.scan(finalText, identity);
|
||||
if (piiWarnings.length > 0) parts.push(`${piiWarnings.length} PII detected`);
|
||||
// Auto-detect unconfigured PPI in the final text
|
||||
const ppiWarnings = AutoDetect.scan(finalText, identity);
|
||||
if (ppiWarnings.length > 0) parts.push(`${ppiWarnings.length} PPI detected`);
|
||||
|
||||
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||
|
||||
// Show PII warnings below stats
|
||||
if (piiWarnings.length > 0) {
|
||||
const piiDiv = document.createElement('div');
|
||||
safeHTML(piiDiv, `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
|
||||
<strong>Unconfigured PII detected:</strong>
|
||||
${piiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
|
||||
</div>`);
|
||||
stats.appendChild(piiDiv);
|
||||
// Show PPI warnings below stats
|
||||
if (ppiWarnings.length > 0) {
|
||||
stats.innerHTML += `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
|
||||
<strong>Unconfigured PPI detected:</strong>
|
||||
${ppiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,7 +737,7 @@ function renderRevealDiff() {
|
||||
const stats = $('#revealStats');
|
||||
|
||||
if (!input) {
|
||||
output.replaceChildren();
|
||||
output.innerHTML = '';
|
||||
stats.textContent = '';
|
||||
return;
|
||||
}
|
||||
@@ -830,7 +783,7 @@ function renderRevealDiff() {
|
||||
`<span class="sub-highlight" title="Was: ${escapeHtml(pair.substitute)}" style="background:#dbeafe;color:#1d4ed8">${escapedReal}</span>`
|
||||
);
|
||||
}
|
||||
safeHTML(output, html);
|
||||
output.innerHTML = html;
|
||||
stats.textContent = `${totalCount} value${totalCount !== 1 ? 's' : ''} revealed`;
|
||||
}
|
||||
|
||||
@@ -911,114 +864,6 @@ function updateStatusDot() {
|
||||
dot.classList.toggle('disabled', !settings.enabled);
|
||||
}
|
||||
|
||||
// --- Popup Domain Management ---
|
||||
|
||||
const POPUP_SUGGESTED_DOMAINS = [
|
||||
{ label: 'Mistral', url: 'https://chat.mistral.ai' },
|
||||
{ label: 'Cohere', url: 'https://coral.cohere.com' },
|
||||
{ label: 'Phind', url: 'https://www.phind.com' },
|
||||
{ label: 'You.com', url: 'https://you.com' },
|
||||
{ label: 'Pi AI', url: 'https://pi.ai' },
|
||||
{ label: 'Discord', url: 'https://discord.com' },
|
||||
{ label: 'Slack', url: 'https://app.slack.com' },
|
||||
{ label: 'Notion', url: 'https://www.notion.so' },
|
||||
{ label: 'Linear', url: 'https://linear.app' },
|
||||
{ label: 'Bitbucket', url: 'https://bitbucket.org' },
|
||||
];
|
||||
|
||||
function normalizeDomain(raw) {
|
||||
let d = raw.trim();
|
||||
if (!d) return null;
|
||||
if (!d.startsWith('http://') && !d.startsWith('https://')) d = 'https://' + d;
|
||||
return d.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
async function popupAddDomain() {
|
||||
const domain = normalizeDomain($('#popupNewDomain').value);
|
||||
if (!domain) return;
|
||||
|
||||
const domains = settings.customDomains || [];
|
||||
if (domains.includes(domain)) return;
|
||||
|
||||
try {
|
||||
const granted = await api.permissions.request({ origins: [domain + '/*'] });
|
||||
if (!granted) return;
|
||||
} catch (e) { /* non-fatal */ }
|
||||
|
||||
domains.push(domain);
|
||||
settings.customDomains = domains;
|
||||
await Storage.saveSettings({ customDomains: domains });
|
||||
$('#popupNewDomain').value = '';
|
||||
renderPopupDomains();
|
||||
renderPopupDomainSuggestions();
|
||||
}
|
||||
|
||||
function renderPopupDomains() {
|
||||
const list = $('#popupDomainList');
|
||||
if (!list) return;
|
||||
const domains = settings.customDomains || [];
|
||||
|
||||
if (domains.length === 0) {
|
||||
safeHTML(list, '<div style="font-size:11px;color:#9ca3af;padding:4px 0">No custom domains added</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
safeHTML(list, domains.map((d, i) => `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:3px 6px;background:#f9fafb;border-radius:4px;margin-bottom:2px">
|
||||
<span style="font-size:11px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">${escapeHtml(d)}</span>
|
||||
<button class="btn-popup-remove-domain" data-index="${i}" style="border:none;background:none;color:#9ca3af;cursor:pointer;font-size:14px;padding:0 2px;line-height:1" title="Remove">×</button>
|
||||
</div>
|
||||
`).join(''));
|
||||
|
||||
list.querySelectorAll('.btn-popup-remove-domain').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const idx = parseInt(btn.dataset.index, 10);
|
||||
const domains = settings.customDomains || [];
|
||||
const removed = domains.splice(idx, 1)[0];
|
||||
settings.customDomains = domains;
|
||||
await Storage.saveSettings({ customDomains: domains });
|
||||
if (removed) {
|
||||
try { await api.permissions.remove({ origins: [removed + '/*'] }); } catch (e) { /* non-fatal */ }
|
||||
}
|
||||
renderPopupDomains();
|
||||
renderPopupDomainSuggestions();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderPopupDomainSuggestions() {
|
||||
const container = $('#popupDomainSuggestions');
|
||||
if (!container) return;
|
||||
const domains = settings.customDomains || [];
|
||||
|
||||
const available = POPUP_SUGGESTED_DOMAINS.filter(s => !domains.includes(s.url));
|
||||
if (available.length === 0) {
|
||||
container.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
safeHTML(container, available.slice(0, 6).map(s =>
|
||||
`<button class="btn-popup-suggest" data-url="${escapeHtml(s.url)}" title="${escapeHtml(s.url)}" style="font-size:10px;padding:2px 6px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;color:#6b7280;white-space:nowrap">+ ${escapeHtml(s.label)}</button>`
|
||||
).join(''));
|
||||
|
||||
container.querySelectorAll('.btn-popup-suggest').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const domain = btn.dataset.url;
|
||||
const domains = settings.customDomains || [];
|
||||
if (domains.includes(domain)) return;
|
||||
try {
|
||||
const granted = await api.permissions.request({ origins: [domain + '/*'] });
|
||||
if (!granted) return;
|
||||
} catch (e) { /* non-fatal */ }
|
||||
domains.push(domain);
|
||||
settings.customDomains = domains;
|
||||
await Storage.saveSettings({ customDomains: domains });
|
||||
renderPopupDomains();
|
||||
renderPopupDomainSuggestions();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Util ---
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
|
||||
Reference in New Issue
Block a user