diff --git a/src/options/options.html b/src/options/options.html index f16ba31..147f151 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -519,12 +519,31 @@
Add domains for self-hosted AI services (like OpenWebUI). The extension will activate on these domains in addition to the built-in ones.
-- 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.
diff --git a/src/options/options.js b/src/options/options.js index e0eba51..cb10a04 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -274,6 +274,19 @@ document.addEventListener('DOMContentLoaded', async () => { $('#newDomain').addEventListener('keydown', (e) => { 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 $('#showHighlights').addEventListener('change', async (e) => { @@ -560,44 +573,119 @@ async function renderLog() { } // --- 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://')) { domain = 'https://' + domain; } - // Strip trailing slashes - domain = domain.replace(/\/+$/, ''); + return domain.replace(/\/+$/, ''); +} +async function addSingleDomain(domain) { const domains = settings.customDomains || []; - if (domains.includes(domain)) { - alert('Domain already added.'); - return; - } + if (domains.includes(domain)) return { added: false, reason: 'duplicate' }; - // Request browser permission for this domain try { - const granted = await api.permissions.request({ - origins: [domain + '/*'], - }); - if (!granted) { - alert('Permission denied. The extension needs access to this domain to work.'); - return; - } + const granted = await api.permissions.request({ origins: [domain + '/*'] }); + if (!granted) return { added: false, reason: 'denied' }; } catch (e) { - // Firefox or older Chrome may not support optional permissions this way console.warn('[Silent Send] Could not request permission:', e); } domains.push(domain); settings.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(); + renderSuggestedDomains(); $('#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, 'All suggestions added!'); + return; + } + + safeHTML(container, available.map(s => + `` + ).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() { const list = $('#domainList'); const domains = settings.customDomains || []; @@ -610,12 +698,86 @@ function renderDomains() { safeHTML(list, domains .map((d, i) => `Sync, encryption, org, version history, import/export, and more
diff --git a/src/popup/popup.js b/src/popup/popup.js index 2f9e75d..677f2c8 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -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 const encEnabled = await Storage._isAtRestEncryptionEnabled(); const encNote = $('#privacyEncNote'); @@ -900,6 +909,114 @@ 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, '