feat: frictionless custom domain management
- Edit domains inline (pencil icon, saves on Enter) - Suggested domains: clickable chips for popular AI/dev/collab sites - Bulk add: paste multiple domains at once (one per line or comma-separated) - Popup domain management: add/remove/suggestions directly from the popup - Permission revoke on removal + suggested list updates dynamically https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
@@ -519,12 +519,31 @@
|
|||||||
<h2>Custom Domains</h2>
|
<h2>Custom Domains</h2>
|
||||||
<p class="section-desc">Add domains for self-hosted AI services (like OpenWebUI). The extension will activate on these domains in addition to the built-in ones.</p>
|
<p class="section-desc">Add domains for self-hosted AI services (like OpenWebUI). The extension will activate on these domains in addition to the built-in ones.</p>
|
||||||
<div class="domain-list" id="domainList"></div>
|
<div class="domain-list" id="domainList"></div>
|
||||||
<div class="add-row">
|
<div class="add-row" style="flex-wrap:wrap;gap:8px">
|
||||||
<input type="text" id="newDomain" placeholder="https://ai.myserver.com" class="input" style="flex:2">
|
<input type="text" id="newDomain" placeholder="https://ai.myserver.com" class="input" style="flex:2;min-width:200px">
|
||||||
<button class="btn btn-primary" id="btnAddDomain">Add Domain</button>
|
<button class="btn btn-primary" id="btnAddDomain">Add Domain</button>
|
||||||
|
<button class="btn" id="btnBulkAddDomains">Bulk Add</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Suggested domains -->
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<label style="font-size:12px;font-weight:500;color:#374151;display:block;margin-bottom:4px">Quick add popular sites:</label>
|
||||||
|
<div id="suggestedDomains" style="display:flex;flex-wrap:wrap;gap:4px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bulk add textarea (hidden by default) -->
|
||||||
|
<div id="bulkDomainSection" style="display:none;margin-top:10px;padding:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px">
|
||||||
|
<label style="font-size:12px;font-weight:500;color:#374151;display:block;margin-bottom:4px">Paste domains (one per line):</label>
|
||||||
|
<textarea id="bulkDomainText" rows="5" placeholder="https://ai.example.com https://openwebui.local myai.company.com" style="width:100%;box-sizing:border-box;font-size:12px;font-family:monospace;padding:8px;border:1px solid #d1d5db;border-radius:6px;resize:vertical"></textarea>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:6px">
|
||||||
|
<button class="btn btn-primary btn-sm" id="btnApplyBulkDomains">Add All</button>
|
||||||
|
<button class="btn btn-sm" id="btnCancelBulkDomains">Cancel</button>
|
||||||
|
<span id="bulkDomainStatus" style="font-size:11px;color:#6b7280;align-self:center"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="section-desc" style="margin-top:8px;margin-bottom:0">
|
<p class="section-desc" style="margin-top:8px;margin-bottom:0">
|
||||||
When you click "Add Domain", your browser will ask you to confirm access. No extra steps needed.
|
When you add a domain, your browser will ask you to confirm access. No extra steps needed.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+184
-22
@@ -274,6 +274,19 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
$('#newDomain').addEventListener('keydown', (e) => {
|
$('#newDomain').addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') addDomain();
|
if (e.key === 'Enter') addDomain();
|
||||||
});
|
});
|
||||||
|
renderSuggestedDomains();
|
||||||
|
|
||||||
|
// Bulk add domains
|
||||||
|
$('#btnBulkAddDomains').addEventListener('click', () => {
|
||||||
|
const section = $('#bulkDomainSection');
|
||||||
|
section.style.display = section.style.display === 'none' ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
$('#btnCancelBulkDomains').addEventListener('click', () => {
|
||||||
|
$('#bulkDomainSection').style.display = 'none';
|
||||||
|
$('#bulkDomainText').value = '';
|
||||||
|
$('#bulkDomainStatus').textContent = '';
|
||||||
|
});
|
||||||
|
$('#btnApplyBulkDomains').addEventListener('click', bulkAddDomains);
|
||||||
|
|
||||||
// Settings listeners
|
// Settings listeners
|
||||||
$('#showHighlights').addEventListener('change', async (e) => {
|
$('#showHighlights').addEventListener('change', async (e) => {
|
||||||
@@ -560,44 +573,119 @@ async function renderLog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Custom Domains ---
|
// --- Custom Domains ---
|
||||||
async function addDomain() {
|
|
||||||
let domain = $('#newDomain').value.trim();
|
|
||||||
if (!domain) return;
|
|
||||||
|
|
||||||
// Normalize: ensure it has a protocol
|
// Suggested popular domains (not already built-in)
|
||||||
|
const SUGGESTED_DOMAINS = [
|
||||||
|
{ label: 'OpenWebUI', url: 'https://openwebui.local' },
|
||||||
|
{ label: 'Ollama Web', url: 'http://localhost:3000' },
|
||||||
|
{ label: 'text-generation-webui', url: 'http://localhost:7860' },
|
||||||
|
{ label: 'Jan.ai', url: 'https://jan.ai' },
|
||||||
|
{ label: 'You.com', url: 'https://you.com' },
|
||||||
|
{ label: 'Phind', url: 'https://www.phind.com' },
|
||||||
|
{ label: 'Cohere', url: 'https://coral.cohere.com' },
|
||||||
|
{ label: 'Mistral', url: 'https://chat.mistral.ai' },
|
||||||
|
{ label: 'Pi AI', url: 'https://pi.ai' },
|
||||||
|
{ label: 'Notion AI', url: 'https://www.notion.so' },
|
||||||
|
{ label: 'Quora', url: 'https://www.quora.com' },
|
||||||
|
{ label: 'Discord', url: 'https://discord.com' },
|
||||||
|
{ label: 'Slack', url: 'https://app.slack.com' },
|
||||||
|
{ label: 'Jira', url: 'https://atlassian.net' },
|
||||||
|
{ label: 'Linear', url: 'https://linear.app' },
|
||||||
|
{ label: 'Bitbucket', url: 'https://bitbucket.org' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeDomain(raw) {
|
||||||
|
let domain = raw.trim();
|
||||||
|
if (!domain) return null;
|
||||||
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
|
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
|
||||||
domain = 'https://' + domain;
|
domain = 'https://' + domain;
|
||||||
}
|
}
|
||||||
// Strip trailing slashes
|
return domain.replace(/\/+$/, '');
|
||||||
domain = domain.replace(/\/+$/, '');
|
}
|
||||||
|
|
||||||
|
async function addSingleDomain(domain) {
|
||||||
const domains = settings.customDomains || [];
|
const domains = settings.customDomains || [];
|
||||||
if (domains.includes(domain)) {
|
if (domains.includes(domain)) return { added: false, reason: 'duplicate' };
|
||||||
alert('Domain already added.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request browser permission for this domain
|
|
||||||
try {
|
try {
|
||||||
const granted = await api.permissions.request({
|
const granted = await api.permissions.request({ origins: [domain + '/*'] });
|
||||||
origins: [domain + '/*'],
|
if (!granted) return { added: false, reason: 'denied' };
|
||||||
});
|
|
||||||
if (!granted) {
|
|
||||||
alert('Permission denied. The extension needs access to this domain to work.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Firefox or older Chrome may not support optional permissions this way
|
|
||||||
console.warn('[Silent Send] Could not request permission:', e);
|
console.warn('[Silent Send] Could not request permission:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
domains.push(domain);
|
domains.push(domain);
|
||||||
settings.customDomains = domains;
|
settings.customDomains = domains;
|
||||||
await Storage.saveSettings({ customDomains: domains });
|
await Storage.saveSettings({ customDomains: domains });
|
||||||
|
return { added: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addDomain() {
|
||||||
|
const domain = normalizeDomain($('#newDomain').value);
|
||||||
|
if (!domain) return;
|
||||||
|
|
||||||
|
const result = await addSingleDomain(domain);
|
||||||
|
if (!result.added) {
|
||||||
|
if (result.reason === 'duplicate') alert('Domain already added.');
|
||||||
|
else alert('Permission denied. The extension needs access to this domain to work.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
renderDomains();
|
renderDomains();
|
||||||
|
renderSuggestedDomains();
|
||||||
$('#newDomain').value = '';
|
$('#newDomain').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bulkAddDomains() {
|
||||||
|
const text = $('#bulkDomainText').value;
|
||||||
|
const lines = text.split(/[\n,]+/).map(l => l.trim()).filter(Boolean);
|
||||||
|
if (lines.length === 0) return;
|
||||||
|
|
||||||
|
let added = 0, skipped = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
const domain = normalizeDomain(line);
|
||||||
|
if (!domain) { skipped++; continue; }
|
||||||
|
const result = await addSingleDomain(domain);
|
||||||
|
if (result.added) added++;
|
||||||
|
else skipped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDomains();
|
||||||
|
renderSuggestedDomains();
|
||||||
|
$('#bulkDomainStatus').textContent = `Added ${added}, skipped ${skipped}`;
|
||||||
|
if (added > 0) $('#bulkDomainText').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSuggestedDomains() {
|
||||||
|
const container = $('#suggestedDomains');
|
||||||
|
if (!container) return;
|
||||||
|
const domains = settings.customDomains || [];
|
||||||
|
|
||||||
|
// Filter out suggestions that are already added
|
||||||
|
const available = SUGGESTED_DOMAINS.filter(s => !domains.includes(s.url));
|
||||||
|
if (available.length === 0) {
|
||||||
|
safeHTML(container, '<span style="font-size:11px;color:#9ca3af">All suggestions added!</span>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
safeHTML(container, available.map(s =>
|
||||||
|
`<button class="btn-suggest-domain" data-url="${escapeHtml(s.url)}" title="${escapeHtml(s.url)}" style="font-size:11px;padding:3px 8px;border:1px solid #d1d5db;border-radius:12px;background:#fff;cursor:pointer;color:#374151;white-space:nowrap">+ ${escapeHtml(s.label)}</button>`
|
||||||
|
).join(''));
|
||||||
|
|
||||||
|
container.querySelectorAll('.btn-suggest-domain').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const domain = btn.dataset.url;
|
||||||
|
const result = await addSingleDomain(domain);
|
||||||
|
if (result.added) {
|
||||||
|
renderDomains();
|
||||||
|
renderSuggestedDomains();
|
||||||
|
} else if (result.reason === 'denied') {
|
||||||
|
alert('Permission denied for ' + domain);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderDomains() {
|
function renderDomains() {
|
||||||
const list = $('#domainList');
|
const list = $('#domainList');
|
||||||
const domains = settings.customDomains || [];
|
const domains = settings.customDomains || [];
|
||||||
@@ -610,12 +698,86 @@ function renderDomains() {
|
|||||||
safeHTML(list, domains
|
safeHTML(list, domains
|
||||||
.map((d, i) => `
|
.map((d, i) => `
|
||||||
<div class="domain-item" style="display:flex;align-items:center;justify-content:space-between;padding:8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
|
<div class="domain-item" style="display:flex;align-items:center;justify-content:space-between;padding:8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
|
||||||
<span style="font-size:13px;font-family:monospace">${escapeHtml(d)}</span>
|
<span class="domain-text" style="font-size:13px;font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis">${escapeHtml(d)}</span>
|
||||||
<button class="btn btn-sm btn-danger btn-remove-domain" data-index="${i}">×</button>
|
<div style="display:flex;gap:4px;margin-left:8px">
|
||||||
|
<button class="btn btn-sm btn-edit-domain" data-index="${i}" title="Edit">✎</button>
|
||||||
|
<button class="btn btn-sm btn-danger btn-remove-domain" data-index="${i}" title="Remove">×</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
.join(''));
|
.join(''));
|
||||||
|
|
||||||
|
// Edit handlers
|
||||||
|
list.querySelectorAll('.btn-edit-domain').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
const domains = settings.customDomains || [];
|
||||||
|
const current = domains[idx];
|
||||||
|
const row = btn.closest('.domain-item');
|
||||||
|
const textEl = row.querySelector('.domain-text');
|
||||||
|
|
||||||
|
// Replace text with input
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.value = current;
|
||||||
|
input.style.cssText = 'flex:1;font-size:12px;font-family:monospace;padding:3px 6px;border:1px solid #3b82f6;border-radius:4px;outline:none;min-width:0';
|
||||||
|
textEl.replaceWith(input);
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
|
||||||
|
// Replace edit button with save button
|
||||||
|
btn.textContent = '\u2713';
|
||||||
|
btn.title = 'Save';
|
||||||
|
btn.style.color = '#10b981';
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const newDomain = normalizeDomain(input.value);
|
||||||
|
if (!newDomain || newDomain === current) {
|
||||||
|
renderDomains();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domains.includes(newDomain)) {
|
||||||
|
alert('Domain already exists.');
|
||||||
|
renderDomains();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request permission for new domain
|
||||||
|
try {
|
||||||
|
const granted = await api.permissions.request({ origins: [newDomain + '/*'] });
|
||||||
|
if (!granted) {
|
||||||
|
alert('Permission denied for ' + newDomain);
|
||||||
|
renderDomains();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) { /* non-fatal */ }
|
||||||
|
|
||||||
|
// Revoke old permission
|
||||||
|
try {
|
||||||
|
await api.permissions.remove({ origins: [current + '/*'] });
|
||||||
|
} catch (e) { /* non-fatal */ }
|
||||||
|
|
||||||
|
domains[idx] = newDomain;
|
||||||
|
settings.customDomains = domains;
|
||||||
|
await Storage.saveSettings({ customDomains: domains });
|
||||||
|
renderDomains();
|
||||||
|
renderSuggestedDomains();
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.onclick = save;
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') save();
|
||||||
|
if (e.key === 'Escape') renderDomains();
|
||||||
|
});
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
// Small delay to allow button click to fire first
|
||||||
|
setTimeout(() => { if (document.contains(input)) renderDomains(); }, 150);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove handlers
|
||||||
list.querySelectorAll('.btn-remove-domain').forEach((btn) => {
|
list.querySelectorAll('.btn-remove-domain').forEach((btn) => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const idx = parseInt(btn.dataset.index, 10);
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
@@ -624,7 +786,6 @@ function renderDomains() {
|
|||||||
settings.customDomains = domains;
|
settings.customDomains = domains;
|
||||||
await Storage.saveSettings({ customDomains: domains });
|
await Storage.saveSettings({ customDomains: domains });
|
||||||
|
|
||||||
// Revoke browser permission for the removed domain
|
|
||||||
if (removed) {
|
if (removed) {
|
||||||
try {
|
try {
|
||||||
await api.permissions.remove({ origins: [removed + '/*'] });
|
await api.permissions.remove({ origins: [removed + '/*'] });
|
||||||
@@ -632,6 +793,7 @@ function renderDomains() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderDomains();
|
renderDomains();
|
||||||
|
renderSuggestedDomains();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,19 @@
|
|||||||
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
|
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
|
||||||
</div>
|
</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">
|
<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>
|
<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>
|
<p class="help-text" style="margin-top:6px;text-align:center">Sync, encryption, org, version history, import/export, and more</p>
|
||||||
|
|||||||
@@ -242,6 +242,15 @@ async function initUnlockedUI() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 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
|
// Update privacy note based on encryption state
|
||||||
const encEnabled = await Storage._isAtRestEncryptionEnabled();
|
const encEnabled = await Storage._isAtRestEncryptionEnabled();
|
||||||
const encNote = $('#privacyEncNote');
|
const encNote = $('#privacyEncNote');
|
||||||
@@ -900,6 +909,114 @@ function updateStatusDot() {
|
|||||||
dot.classList.toggle('disabled', !settings.enabled);
|
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 ---
|
// --- Util ---
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|||||||
Reference in New Issue
Block a user