diff --git a/src/lib/storage.js b/src/lib/storage.js index 20063dd..70916d1 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -67,14 +67,17 @@ const Storage = { await this.saveMappings(filtered); }, - // --- Identity (Smart Patterns) --- + // --- Identity Profiles (Smart Patterns) --- - async getIdentity() { - const result = await api.storage.local.get(KEYS.IDENTITY); - return result[KEYS.IDENTITY] || { + _emptyProfile() { + return { + id: crypto.randomUUID(), + name: 'Personal', + active: true, emails: [], names: [], usernames: [], + hostnames: [], phones: [], catchAllEmail: '', emailDomains: [], @@ -82,8 +85,86 @@ const Storage = { }; }, + async getProfiles() { + const result = await api.storage.local.get(KEYS.IDENTITY); + const data = result[KEYS.IDENTITY]; + if (data?.profiles) return data.profiles; + return []; + }, + + async saveProfiles(profiles) { + await api.storage.local.set({ [KEYS.IDENTITY]: { profiles } }); + }, + + async addProfile(name) { + const profiles = await this.getProfiles(); + const profile = { ...this._emptyProfile(), name: name || `Profile ${profiles.length + 1}` }; + profiles.push(profile); + await this.saveProfiles(profiles); + return profile; + }, + + async updateProfile(id, updates) { + const profiles = await this.getProfiles(); + const idx = profiles.findIndex((p) => p.id === id); + if (idx === -1) return null; + profiles[idx] = { ...profiles[idx], ...updates }; + await this.saveProfiles(profiles); + return profiles[idx]; + }, + + async deleteProfile(id) { + const profiles = await this.getProfiles(); + const filtered = profiles.filter((p) => p.id !== id); + await this.saveProfiles(filtered); + }, + + // Merged identity: combines all active profiles into one identity object + // for the substitution engine (which expects a single identity) + async getIdentity() { + const profiles = await this.getProfiles(); + const active = profiles.filter((p) => p.active); + + if (active.length === 0) { + return { emails: [], names: [], usernames: [], hostnames: [], phones: [], + catchAllEmail: '', emailDomains: [], enabled: { emails: true, names: true, usernames: true, phones: true, paths: true } }; + } + + // Merge all active profiles + const merged = { + emails: [], + names: [], + usernames: [], + hostnames: [], + phones: [], + catchAllEmail: '', + emailDomains: [], + enabled: { emails: true, names: true, usernames: true, phones: true, paths: true }, + }; + + for (const p of active) { + merged.emails.push(...(p.emails || [])); + merged.names.push(...(p.names || [])); + merged.usernames.push(...(p.usernames || [])); + merged.hostnames.push(...(p.hostnames || [])); + merged.phones.push(...(p.phones || [])); + if (p.catchAllEmail && !merged.catchAllEmail) merged.catchAllEmail = p.catchAllEmail; + merged.emailDomains.push(...(p.emailDomains || [])); + } + + return merged; + }, + + // Legacy compat: saveIdentity saves to first profile async saveIdentity(identity) { - await api.storage.local.set({ [KEYS.IDENTITY]: identity }); + const profiles = await this.getProfiles(); + if (profiles.length === 0) { + const profile = { ...this._emptyProfile(), ...identity }; + await this.saveProfiles([profile]); + } else { + profiles[0] = { ...profiles[0], ...identity }; + await this.saveProfiles(profiles); + } }, // --- Activity Log --- diff --git a/src/popup/popup.css b/src/popup/popup.css index ded12a4..9c1bc09 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -276,6 +276,28 @@ body { font-weight: 600; } +/* Profile bar */ +.profile-bar { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 8px; +} + +.profile-bar .select { + font-weight: 500; +} + +.profile-bar .btn-icon { + width: 28px; + height: 28px; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} + /* Identity tab */ .id-section { margin-bottom: 12px; diff --git a/src/popup/popup.html b/src/popup/popup.html index 6871f99..4c36a69 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -46,7 +46,18 @@
-

Tell Silent Send who you are. It auto-catches all variations.

+ +
+ + + + + +
+

Each profile is a person/identity. All active profiles are protected simultaneously.

Names
diff --git a/src/popup/popup.js b/src/popup/popup.js index 0e24c94..6edc5c6 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -6,7 +6,9 @@ import api from '../lib/browser-polyfill.js'; // --- State --- let mappings = []; -let identity = {}; +let identity = {}; // merged identity (all active profiles) +let profiles = []; // all profiles +let currentProfileId = null; // currently selected profile for editing let settings = {}; // --- DOM refs --- @@ -16,12 +18,21 @@ const $$ = (sel) => document.querySelectorAll(sel); // --- Init --- document.addEventListener('DOMContentLoaded', async () => { mappings = await Storage.getMappings(); + profiles = await Storage.getProfiles(); identity = await Storage.getIdentity(); settings = await Storage.getSettings(); + // Initialize profiles — create default if none exist + if (profiles.length === 0) { + const p = await Storage.addProfile('Personal'); + profiles = [p]; + } + currentProfileId = profiles[0].id; + + renderProfileSelector(); + loadIdentityForm(); renderMappings(); renderActivity(); - loadIdentityForm(); updateStatusDot(); checkFirstRun(); @@ -37,12 +48,20 @@ document.addEventListener('DOMContentLoaded', async () => { if (tab.dataset.tab === 'activity') renderActivity(); if (tab.dataset.tab === 'test') { - // Reload identity from storage in case it was just saved + // Reload merged identity from storage in case profiles were just saved Storage.getIdentity().then((id) => { identity = id; updateIdentityStatus(); }); } + if (tab.dataset.tab === 'identity') { + // Reload profiles + Storage.getProfiles().then((p) => { + profiles = p; + renderProfileSelector(); + loadIdentityForm(); + }); + } }); }); @@ -70,6 +89,57 @@ document.addEventListener('DOMContentLoaded', async () => { $('#btnReveal').classList.toggle('active', settings.revealMode); + // Profile controls + $('#profileSelect').addEventListener('change', (e) => { + currentProfileId = e.target.value; + loadIdentityForm(); + }); + + $('#btnAddProfile').addEventListener('click', async () => { + const name = prompt('Profile name:', `Profile ${profiles.length + 1}`); + if (!name) return; + const p = await Storage.addProfile(name); + profiles = await Storage.getProfiles(); + currentProfileId = p.id; + renderProfileSelector(); + loadIdentityForm(); + checkFirstRun(); + }); + + $('#btnRenameProfile').addEventListener('click', async () => { + const profile = profiles.find(p => p.id === currentProfileId); + if (!profile) return; + const name = prompt('Rename profile:', profile.name); + if (!name) return; + await Storage.updateProfile(currentProfileId, { name }); + profiles = await Storage.getProfiles(); + renderProfileSelector(); + }); + + $('#btnDeleteProfile').addEventListener('click', async () => { + if (profiles.length <= 1) { + alert('Cannot delete the last profile.'); + return; + } + const profile = profiles.find(p => p.id === currentProfileId); + if (!confirm(`Delete profile "${profile?.name}"?`)) return; + await Storage.deleteProfile(currentProfileId); + profiles = await Storage.getProfiles(); + currentProfileId = profiles[0]?.id; + renderProfileSelector(); + loadIdentityForm(); + identity = await Storage.getIdentity(); + checkFirstRun(); + }); + + $('#profileActive').addEventListener('change', async (e) => { + await Storage.updateProfile(currentProfileId, { active: e.target.checked }); + profiles = await Storage.getProfiles(); + identity = await Storage.getIdentity(); + renderProfileSelector(); + checkFirstRun(); + }); + // Save identity $('#btnSaveIdentity').addEventListener('click', saveIdentity); @@ -108,40 +178,45 @@ document.addEventListener('DOMContentLoaded', async () => { }); }); +// --- Profiles --- +function renderProfileSelector() { + const select = $('#profileSelect'); + select.innerHTML = profiles.map(p => + `` + ).join(''); + + const profile = profiles.find(p => p.id === currentProfileId); + $('#profileActive').checked = profile?.active ?? true; +} + // --- Identity --- function loadIdentityForm() { - const first = (identity.names || []).find(n => n.type === 'first'); - const last = (identity.names || []).find(n => n.type === 'last'); - const email = (identity.emails || [])[0]; - const user = (identity.usernames || [])[0]; - const phone = (identity.phones || [])[0]; + const profile = profiles.find(p => p.id === currentProfileId); + if (!profile) return; - if (first) { - $('#idFirstReal').value = first.real || ''; - $('#idFirstSub').value = first.substitute || ''; - } - if (last) { - $('#idLastReal').value = last.real || ''; - $('#idLastSub').value = last.substitute || ''; - } - if (email) { - $('#idEmailReal').value = email.real || ''; - $('#idEmailSub').value = email.substitute || ''; - } - $('#idCatchAllEmail').value = identity.catchAllEmail || ''; - if (user) { - $('#idUserReal').value = user.real || ''; - $('#idUserSub').value = user.substitute || ''; - } - const host = (identity.hostnames || [])[0]; - if (host) { - $('#idHostReal').value = host.real || ''; - $('#idHostSub').value = host.substitute || ''; - } - if (phone) { - $('#idPhoneReal').value = phone.real || ''; - $('#idPhoneSub').value = phone.substitute || ''; - } + const first = (profile.names || []).find(n => n.type === 'first'); + const last = (profile.names || []).find(n => n.type === 'last'); + const email = (profile.emails || [])[0]; + const user = (profile.usernames || [])[0]; + const host = (profile.hostnames || [])[0]; + const phone = (profile.phones || [])[0]; + + $('#idFirstReal').value = first?.real || ''; + $('#idFirstSub').value = first?.substitute || ''; + $('#idLastReal').value = last?.real || ''; + $('#idLastSub').value = last?.substitute || ''; + $('#idEmailReal').value = email?.real || ''; + $('#idEmailSub').value = email?.substitute || ''; + $('#idCatchAllEmail').value = profile.catchAllEmail || ''; + $('#idUserReal').value = user?.real || ''; + $('#idUserSub').value = user?.substitute || ''; + $('#idHostReal').value = host?.real || ''; + $('#idHostSub').value = host?.substitute || ''; + $('#idPhoneReal').value = phone?.real || ''; + $('#idPhoneSub').value = phone?.substitute || ''; + $('#profileActive').checked = profile.active ?? true; } async function saveIdentity() { @@ -185,18 +260,20 @@ async function saveIdentity() { phones.push({ real: phoneReal, substitute: phoneSub }); } - identity = { + const profileData = { names, emails, usernames, hostnames, phones, catchAllEmail: $('#idCatchAllEmail').value.trim(), - emailDomains: identity.emailDomains || [], - enabled: identity.enabled || { emails: true, names: true, usernames: true, phones: true, paths: true }, + emailDomains: [], + enabled: { emails: true, names: true, usernames: true, phones: true, paths: true }, }; - await Storage.saveIdentity(identity); + await Storage.updateProfile(currentProfileId, profileData); + profiles = await Storage.getProfiles(); + identity = await Storage.getIdentity(); checkFirstRun(); // Flash save button