diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 74da5d5..aa8544d 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -1,364 +1,44 @@ -export default defineBackground(() => { - console.log("Gmail Alias Toolkit background started"); - - // Create context menu on install - browser.runtime.onInstalled.addListener(async () => { - await createContextMenus(); - await updateBadge(); - }); +import { getHostnameFromUrl, normalizeDomain } from '../src/utils/domain'; +import { detectCategory } from '../src/utils/categoryDetector'; +import { generateAliasSuggestions, buildPlusAlias } from '../src/utils/aliasGenerator'; +import { getActiveBaseEmail, loadAliasData, touchAlias } from '../src/utils/storage'; - // Recreate context menus when settings change - browser.storage.onChanged.addListener(async (changes) => { - if (changes.app_settings) { - await browser.contextMenus.removeAll(); - await createContextMenus(); - // Update badge when app_settings changes (includes showBadge toggle) - await updateBadge(); - } - - // Update badge when history or accounts change - const changedKeys = Object.keys(changes); - const shouldUpdateBadge = changedKeys.some( - (key) => - key.startsWith("gmail_alias_recent_") || - key.startsWith("alias_stats_") || - key === "email_accounts" - ); - if (shouldUpdateBadge) { - await updateBadge(); - } - }); +export default defineBackground(() => { + browser.runtime.onInstalled.addListener(async () => { await createContextMenus(); await updateBadge(); }); + browser.storage.onChanged.addListener(async () => { await updateBadge(); }); - // Function to create context menus async function createContextMenus() { - // Parent menu - browser.contextMenus.create({ - id: "gmail-alias-parent", - title: "Gmail Alias Toolkit", - contexts: ["editable"], - }); - - // Random email submenu - browser.contextMenus.create({ - id: "fill-random-email", - parentId: "gmail-alias-parent", - title: "🎲 Random Email Alias", - contexts: ["editable"], - }); - - // Custom tag submenu - sync with user's presets - browser.contextMenus.create({ - id: "custom-tag-parent", - parentId: "gmail-alias-parent", - title: "📝 Custom Tags", - contexts: ["editable"], - }); - - // Load custom presets from storage - const result = await browser.storage.local.get("app_settings"); - const customPresets = result.app_settings?.customPresets || []; - - if (customPresets.length > 0) { - customPresets.forEach((preset: any) => { - browser.contextMenus.create({ - id: `tag-${preset.tag}`, - parentId: "custom-tag-parent", - title: `${preset.label} (+${preset.tag})`, - contexts: ["editable"], - }); - }); - } else { - // Show message if no presets - browser.contextMenus.create({ - id: "no-presets", - parentId: "custom-tag-parent", - title: "No presets - Add in Settings", - contexts: ["editable"], - enabled: false, - }); - } - - // Gmail tricks submenu - browser.contextMenus.create({ - id: "gmail-tricks-parent", - parentId: "gmail-alias-parent", - title: "✨ Gmail Tricks", - contexts: ["editable"], - }); - - browser.contextMenus.create({ - id: "trick-dot", - parentId: "gmail-tricks-parent", - title: "Dot Variation", - contexts: ["editable"], - }); - - browser.contextMenus.create({ - id: "trick-googlemail", - parentId: "gmail-tricks-parent", - title: "Googlemail Domain", - contexts: ["editable"], - }); - - browser.contextMenus.create({ - id: "trick-nodots", - parentId: "gmail-tricks-parent", - title: "Remove All Dots", - contexts: ["editable"], - }); + await browser.contextMenus.removeAll().catch(() => undefined); + browser.contextMenus.create({ id:'gmail-alias-parent', title:'Gmail Alias Toolkit', contexts:['editable'] }); + browser.contextMenus.create({ id:'insert-suggested-alias', parentId:'gmail-alias-parent', title:'Insert suggested alias', contexts:['editable'] }); + browser.contextMenus.create({ id:'copy-suggested-alias', parentId:'gmail-alias-parent', title:'Copy suggested alias', contexts:['editable'] }); + browser.contextMenus.create({ id:'use-previous-alias', parentId:'gmail-alias-parent', title:'Use previous alias for this site', contexts:['editable'] }); + browser.contextMenus.create({ id:'generate-random-alias', parentId:'gmail-alias-parent', title:'Generate random alias', contexts:['editable'] }); + browser.contextMenus.create({ id:'fill-random-email', parentId:'gmail-alias-parent', title:'🎲 Random Email Alias', contexts:['editable'] }); } - // Handle context menu clicks browser.contextMenus.onClicked.addListener(async (info, tab) => { if (!tab?.id) return; - - // Get base email from storage - const result = await browser.storage.local.get([ - "email_accounts", - "base_email", - "app_settings", - ]); - let baseEmail = "your.email@gmail.com"; - - if (result.email_accounts && Array.isArray(result.email_accounts)) { - const activeAccount = result.email_accounts.find( - (acc: any) => acc.isActive - ); - if (activeAccount) { - baseEmail = activeAccount.email; - } - } else if (result.base_email) { - baseEmail = result.base_email; - } - - const [username, domain] = baseEmail.split("@"); - let emailToFill = ""; - - if (info.menuItemId === "fill-random-email") { - // Generate random email - const format = result.app_settings?.randomFormat || "private-mail"; - let randomTag = ""; - - switch (format) { - case "private-mail": - const chars = "abcdefghijklmnopqrstuvwxyz"; - randomTag = Array.from( - { length: 8 }, - () => chars[Math.floor(Math.random() * chars.length)] - ).join(""); - break; - case "alphanumeric": - const alphanum = "abcdefghijklmnopqrstuvwxyz0123456789"; - randomTag = Array.from( - { length: 10 }, - () => alphanum[Math.floor(Math.random() * alphanum.length)] - ).join(""); - break; - case "words": - const words = [ - "alpha", - "beta", - "gamma", - "delta", - "echo", - "foxtrot", - "golf", - "hotel", - ]; - const word1 = words[Math.floor(Math.random() * words.length)]; - const word2 = words[Math.floor(Math.random() * words.length)]; - const num = Math.floor(Math.random() * 100); - randomTag = `${word1}${word2}${num}`; - break; - case "timestamp": - randomTag = Date.now().toString(); - break; - } - - emailToFill = `${username}+${randomTag}@${domain}`; - } else if (info.menuItemId?.startsWith("tag-")) { - // Custom tag from preset - const tag = info.menuItemId.replace("tag-", ""); - emailToFill = `${username}+${tag}@${domain}`; - } else if (info.menuItemId === "trick-dot") { - // Dot variation - insert dot at random position - const pos = Math.floor(Math.random() * (username.length - 1)) + 1; - const dottedUsername = username.slice(0, pos) + "." + username.slice(pos); - emailToFill = `${dottedUsername}@${domain}`; - } else if (info.menuItemId === "trick-googlemail") { - // Googlemail domain - const altDomain = domain === "gmail.com" ? "googlemail.com" : "gmail.com"; - emailToFill = `${username}@${altDomain}`; - } else if (info.menuItemId === "trick-nodots") { - // Remove all dots - const noDots = username.replace(/\./g, ""); - emailToFill = `${noDots}@${domain}`; - } - - if (emailToFill) { - // Save to history and statistics - await saveToHistory(emailToFill, result.app_settings?.maxHistory || 20); - - // Send message to content script to fill the input - browser.tabs.sendMessage(tab.id, { - action: "fillEmail", - email: emailToFill, - }); - } + const email = await resolveAlias(String(info.menuItemId), tab.url); + if (!email) return; + if (String(info.menuItemId).startsWith('copy')) await navigator.clipboard.writeText(email).catch(() => undefined); + const res = await browser.tabs.sendMessage(tab.id, { action:'autofillAlias', email }).catch(() => ({ ok:false })); + await saveToLegacyHistory(email); + const hostname = tab.url ? getHostnameFromUrl(tab.url) : null; + if (hostname) await touchAlias(hostname); }); - // Helper function to update badge - async function updateBadge() { - try { - // Check badge display setting - const settingsResult = await browser.storage.local.get("app_settings"); - const badgeDisplay = - settingsResult.app_settings?.badgeDisplay ?? "all-time"; - - if (badgeDisplay === "none") { - await browser.action.setBadgeText({ text: "" }); - return; - } - - // Get active account - const accountResult = await browser.storage.local.get([ - "email_accounts", - "base_email", - ]); - let activeEmail = "your.email@gmail.com"; - - if ( - accountResult.email_accounts && - Array.isArray(accountResult.email_accounts) - ) { - const activeAccount = accountResult.email_accounts.find( - (acc: any) => acc.isActive - ); - if (activeAccount) { - activeEmail = activeAccount.email; - } - } else if (accountResult.base_email) { - activeEmail = accountResult.base_email; - } - - // Get history for active account - const historyKey = getAccountStorageKey( - activeEmail, - "gmail_alias_recent" - ); - const statsKey = getAccountStorageKey(activeEmail, "alias_stats"); - const result = await browser.storage.local.get([historyKey, statsKey]); - const recentAliases = result[historyKey] || []; - const aliasStats = result[statsKey] || { total: 0, tags: {} }; - - let count = 0; - const now = new Date(); - - switch (badgeDisplay) { - case "total": - count = recentAliases.length; - break; - case "all-time": - count = aliasStats.total || 0; - break; - case "today": - const today = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate() - ).getTime(); - count = recentAliases.filter((a: any) => a.timestamp >= today).length; - break; - case "week": - const weekAgo = new Date( - now.getTime() - 7 * 24 * 60 * 60 * 1000 - ).getTime(); - count = recentAliases.filter( - (a: any) => a.timestamp >= weekAgo - ).length; - break; - } - - // Update badge - if (count > 0) { - await browser.action.setBadgeText({ text: count.toString() }); - await browser.action.setBadgeBackgroundColor({ color: "#3B82F6" }); // Blue - await browser.action.setBadgeTextColor({ color: "#FFFFFF" }); // White text - } else { - await browser.action.setBadgeText({ text: "" }); - } - } catch (error) { - console.error("Error updating badge:", error); - } - } - - // Helper function to get account-specific storage key - function getAccountStorageKey(email: string, suffix: string): string { - const sanitized = email.replace(/[^a-zA-Z0-9]/g, "_"); - return `${suffix}_${sanitized}`; + async function resolveAlias(menuId:string, url?:string) { + const hostname = url ? getHostnameFromUrl(url) : null; + const data = await loadAliasData(); + if (hostname && (menuId === 'use-previous-alias' || menuId === 'copy-previous-alias') && data.siteAliases[hostname]) return data.siteAliases[hostname].alias; + if (hostname && menuId !== 'generate-random-alias' && data.siteAliases[hostname]) return data.siteAliases[hostname].alias; + const baseEmail = await getActiveBaseEmail(); if (!baseEmail) return ''; + const keyword = hostname ? normalizeDomain(hostname) : 'site'; + if (menuId === 'generate-random-alias' || menuId === 'fill-random-email') return buildPlusAlias(baseEmail, `${keyword}-${Math.random().toString(36).slice(2,6)}`); + return generateAliasSuggestions({ baseEmail, domainKeyword: keyword, category: detectCategory(keyword, hostname || '') })[0]?.alias || ''; } - // Helper function to save email to history and stats - async function saveToHistory(email: string, maxRecent: number) { - // Get active account - const accountResult = await browser.storage.local.get([ - "email_accounts", - "base_email", - ]); - let activeEmail = "your.email@gmail.com"; - - if ( - accountResult.email_accounts && - Array.isArray(accountResult.email_accounts) - ) { - const activeAccount = accountResult.email_accounts.find( - (acc: any) => acc.isActive - ); - if (activeAccount) { - activeEmail = activeAccount.email; - } - } else if (accountResult.base_email) { - activeEmail = accountResult.base_email; - } - - // Use account-specific storage keys - const historyKey = getAccountStorageKey(activeEmail, "gmail_alias_recent"); - const statsKey = getAccountStorageKey(activeEmail, "alias_stats"); - - // Get current history - const result = await browser.storage.local.get([historyKey, statsKey]); - const recentAliases = result[historyKey] || []; - - // Add to history (remove duplicates, add to top) - const newAlias = { - email, - timestamp: Date.now(), - }; - - const updated = [ - newAlias, - ...recentAliases.filter((a: any) => a.email !== email), - ].slice(0, maxRecent); - - // Update statistics - let stats = result[statsKey] || { total: 0, tags: {} }; - stats.total = (stats.total || 0) + 1; - - // Extract tag from email (if it has + addressing) - const tagMatch = email.match(/\+([^@]+)@/); - if (tagMatch) { - const tag = tagMatch[1]; - stats.tags = stats.tags || {}; - stats.tags[tag] = (stats.tags[tag] || 0) + 1; - } - - // Save to storage with account-specific keys - await browser.storage.local.set({ - [historyKey]: updated, - [statsKey]: stats, - }); - - // Update badge - await updateBadge(); - } + async function updateBadge() { const data=await loadAliasData().catch(()=>null); const count=data ? Object.keys(data.siteAliases).length : 0; await browser.action.setBadgeText({ text: count ? String(count) : '' }); await browser.action.setBadgeBackgroundColor({ color:'#2563eb' }); } + async function saveToLegacyHistory(email:string) { const r:any=await browser.storage.local.get(['base_email','app_settings']); const active=String(r.base_email || ''); const key=`gmail_alias_recent_${active.replace(/[^a-zA-Z0-9]/g,'_')}`; const old:any[]=(await browser.storage.local.get(key) as any)[key] || []; await browser.storage.local.set({[key]:[{email,timestamp:Date.now()},...old.filter((a:any)=>a.email!==email)].slice(0,r.app_settings?.maxHistory || 20)}); } }); diff --git a/entrypoints/content.ts b/entrypoints/content.ts index 9f8704f..72a213b 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -1,45 +1,14 @@ +import { autofillEmail } from '../src/utils/autofill'; + export default defineContentScript({ - matches: [""], + matches: [''], main() { - // Listen for messages from background script browser.runtime.onMessage.addListener((message) => { - if (message.action === "fillEmail" && message.email) { - // Get the active element (the input field that was right-clicked) - const activeElement = document.activeElement; - - if ( - activeElement && - (activeElement.tagName === "INPUT" || - activeElement.tagName === "TEXTAREA" || - activeElement.isContentEditable) - ) { - if ( - activeElement instanceof HTMLInputElement || - activeElement instanceof HTMLTextAreaElement - ) { - // Fill input or textarea - activeElement.value = message.email; - - // Trigger input event for frameworks like React/Vue - activeElement.dispatchEvent(new Event("input", { bubbles: true })); - activeElement.dispatchEvent(new Event("change", { bubbles: true })); - } else if (activeElement.isContentEditable) { - // Fill contentEditable element - activeElement.textContent = message.email; - - // Trigger input event - activeElement.dispatchEvent(new Event("input", { bubbles: true })); - } - - // Flash effect to show it was filled - const originalBg = (activeElement as HTMLElement).style - .backgroundColor; - (activeElement as HTMLElement).style.backgroundColor = "#d1fae5"; - setTimeout(() => { - (activeElement as HTMLElement).style.backgroundColor = originalBg; - }, 500); - } + if ((message.action === 'fillEmail' || message.action === 'autofillAlias') && message.email) { + const ok = autofillEmail(message.email); + return Promise.resolve({ ok }); } + return undefined; }); }, }); diff --git a/entrypoints/popup/App.css b/entrypoints/popup/App.css index e15d344..54993f5 100644 --- a/entrypoints/popup/App.css +++ b/entrypoints/popup/App.css @@ -1,15 +1 @@ -/* Custom animations and styles */ -@keyframes fade-in { - from { - opacity: 0; - transform: translate(-50%, 10px); - } - to { - opacity: 1; - transform: translate(-50%, 0); - } -} - -.animate-fade-in { - animation: fade-in 0.2s ease-out; -} +:root{--bg:#ffffff;--text:#111827;--muted:#6b7280;--border:#e5e7eb;--card:#f9fafb;--primary:#2563eb;--warn:#b45309;--danger:#dc2626} [data-theme="dark"]{--bg:#111827;--text:#f9fafb;--muted:#9ca3af;--border:#374151;--card:#1f2937;--primary:#60a5fa;--warn:#fbbf24;--danger:#f87171} body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.app{width:410px;min-height:520px;background:var(--bg);color:var(--text)}header{padding:16px;background:linear-gradient(135deg,var(--primary),#1d4ed8);color:white}h1{font-size:18px;margin:0}h2{font-size:16px;margin:0 0 12px}header p{margin:2px 0 12px;color:#dbeafe;font-size:12px}nav,.actions{display:flex;gap:8px;flex-wrap:wrap}button,select,input,textarea{border:1px solid var(--border);border-radius:8px;padding:8px 10px;background:var(--bg);color:var(--text);font-size:12px}button{cursor:pointer}button:hover{border-color:var(--primary)}button:disabled{opacity:.45;cursor:not-allowed}textarea{min-height:86px}.card,.privacy{margin:12px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--card)}.label{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:700;margin:10px 0 6px}.muted{color:var(--muted);font-size:12px}.warn{color:var(--warn);font-size:12px}.aliasBox{border:1px dashed var(--primary);border-radius:10px;background:var(--bg);padding:12px;font-weight:700;overflow-wrap:anywhere}.suggestion{display:flex;justify-content:space-between;align-items:center;gap:8px;border:1px solid var(--border);border-radius:10px;padding:8px;margin-top:6px;background:var(--bg);font-size:12px;overflow-wrap:anywhere}.previous,.info{margin-top:12px;border:1px solid var(--border);background:var(--bg);border-radius:10px;padding:10px;font-size:12px}.info span{color:var(--warn)}.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px}table{width:100%;border-collapse:collapse;font-size:11px}th,td{border-top:1px solid var(--border);padding:6px;text-align:left;vertical-align:top}td:nth-child(2){overflow-wrap:anywhere;max-width:125px}.privacy{font-size:12px;line-height:1.5}.toast{position:fixed;left:50%;bottom:14px;transform:translateX(-50%);background:var(--text);color:var(--bg);padding:10px 14px;border-radius:999px;font-size:12px;box-shadow:0 8px 30px #0004;animation:fade-in .2s ease-out}@keyframes fade-in{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}} diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index a123fa5..8efe407 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -1,1175 +1,54 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import './App.css'; -import Settings from './components/Settings'; -import Statistics from './components/Statistics'; -import GmailTricks from './components/GmailTricks'; -import WelcomeScreen from './components/WelcomeScreen'; - -interface Alias { - email: string; - timestamp: number; -} - -interface Preset { - id: string; - label: string; - tag: string; -} - -interface AppSettings { - customPresets: Preset[]; - maxHistory: number; - tags?: Record; - total?: number; - randomFormat?: 'private-mail' | 'alphanumeric' | 'words' | 'timestamp'; -} - -interface StorageResult { - [key: string]: any; - gmail_alias_recent?: Alias[]; - base_email?: string; - app_settings?: AppSettings; - alias_stats?: { - total: number; - tags: Record; - }; +import { getHostnameFromUrl, normalizeDomain } from '../../src/utils/domain'; +import { detectCategory, CATEGORY_MAP } from '../../src/utils/categoryDetector'; +import { buildPlusAlias, generateAliasSuggestions, isGmailAddress } from '../../src/utils/aliasGenerator'; +import { buildGmailFilterQuery, buildGmailSearchUrl, suggestedGmailLabel } from '../../src/utils/gmailFilter'; +import { calculateAliasQuality } from '../../src/utils/qualityScore'; +import { deleteSiteAlias, loadAliasData, migrateStorageIfNeeded, saveSiteAlias, touchAlias, updateAliasStatus } from '../../src/utils/storage'; +import type { AliasCategory, AliasStatus, SiteAlias } from '../../src/types/alias'; +import type { AliasSettings } from '../../src/types/settings'; + +const statuses: AliasStatus[] = ['normal','important','spam','leaked','inactive']; +const categories = Object.keys(CATEGORY_MAP) as AliasCategory[]; +const id = () => `${Date.now()}-${Math.random().toString(36).slice(2)}`; +const nowIso = () => new Date().toISOString(); + +export default function App() { + const [hostname,setHostname]=useState(null); + const [manualHost,setManualHost]=useState(''); + const [settings,setSettings]=useState({baseEmails:[],defaultAliasFormat:'domain',theme:'system',autoDetectCategory:true,autofillFocusedInputFirst:true,fallbackToCopyWhenNoInput:true,gmailAccountIndex:0}); + const [aliases,setAliases]=useState>({}); + const [toast,setToast]=useState(''); + const [view,setView]=useState<'popup'|'dashboard'|'settings'>('popup'); + const [purpose,setPurpose]=useState(''); + const [search,setSearch]=useState(''); const [statusFilter,setStatusFilter]=useState<'all'|AliasStatus>('all'); const [categoryFilter,setCategoryFilter]=useState<'all'|AliasCategory>('all'); const [sort,setSort]=useState<'lastUsedAt'|'createdAt'|'useCount'>('lastUsedAt'); + const activeHost = hostname || (manualHost.trim() || null); + const domainKeyword = activeHost ? normalizeDomain(activeHost) : ''; + const category = domainKeyword ? detectCategory(domainKeyword, activeHost || '') : 'other'; + const baseEmail = settings.defaultBaseEmail || settings.baseEmails[0] || ''; + const previous = activeHost ? aliases[activeHost] : undefined; + const suggestions = useMemo(()=>{ try { return baseEmail && domainKeyword ? generateAliasSuggestions({baseEmail,domainKeyword,category,purpose}) : []; } catch { return []; } },[baseEmail,domainKeyword,category,purpose]); + const selected = previous?.alias || suggestions[0]?.alias || ''; + const quality = selected ? calculateAliasQuality(previous || suggestions[0]) : null; + + useEffect(()=>{ (async()=>{ await migrateStorageIfNeeded(); const data=await loadAliasData(); setSettings(data.settings); setAliases(data.siteAliases); const [tab]=await browser.tabs.query({active:true,currentWindow:true}); setHostname(tab?.url ? getHostnameFromUrl(tab.url) : null); applyTheme(data.settings.theme); })(); },[]); + const showToast=(m:string)=>{setToast(m); setTimeout(()=>setToast(''),2200);}; + const applyTheme=(theme:AliasSettings['theme'])=>{ document.documentElement.dataset.theme = theme === 'system' && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : theme === 'dark' ? 'dark' : 'light'; }; + const persistSettings=async(next:AliasSettings)=>{ setSettings(next); applyTheme(next.theme); await browser.storage.local.set({settings:next, base_email:next.defaultBaseEmail || next.baseEmails[0] || ''}); }; + const save = async(alias=selected)=>{ if(!activeHost || !alias) return; const existing=aliases[activeHost]; const site:SiteAlias={ id:existing?.id || id(), hostname:activeHost, normalizedDomain:domainKeyword, baseEmail, alias, category, note:existing?.note || '', status:existing?.status || 'normal', createdAt:existing?.createdAt || nowIso(), updatedAt:nowIso(), lastUsedAt:existing?.lastUsedAt, useCount:existing?.useCount || 0 }; await saveSiteAlias(site); setAliases({...aliases,[activeHost]:site}); showToast('Saved locally'); }; + const copy = async(alias=selected)=>{ await navigator.clipboard.writeText(alias); if(activeHost && aliases[activeHost]) await touchAlias(activeHost); showToast('Copied'); }; + const autofill = async(alias=selected)=>{ const [tab]=await browser.tabs.query({active:true,currentWindow:true}); let ok=false; if(tab?.id) ok=!!(await browser.tabs.sendMessage(tab.id,{action:'autofillAlias',email:alias}).catch(()=>({ok:false}))).ok; if(!ok && settings.fallbackToCopyWhenNoInput) await navigator.clipboard.writeText(alias); if(activeHost && aliases[activeHost]) await touchAlias(activeHost); showToast(ok?'Autofilled':'No email input found; copied instead'); }; + const exportJson=async()=>{ const data=await loadAliasData(); download(`gmail-alias-toolkit-${Date.now()}.json`, JSON.stringify({version:1,exportedAt:nowIso(),siteAliases:data.siteAliases,settings:data.settings},null,2),'application/json'); }; + const exportCsv=()=>{ const rows=['hostname,normalizedDomain,alias,baseEmail,category,status,note,createdAt,lastUsedAt,useCount',...Object.values(aliases).map(a=>[a.hostname,a.normalizedDomain,a.alias,a.baseEmail,a.category,a.status,a.note||'',a.createdAt,a.lastUsedAt||'',a.useCount].map(v=>`"${String(v).replace(/"/g,'""')}"`).join(','))]; download(`gmail-aliases-${Date.now()}.csv`,rows.join('\n'),'text/csv'); }; + const importJson=()=>{ const input=document.createElement('input'); input.type='file'; input.accept='application/json'; input.onchange=async()=>{ const file=input.files?.[0]; if(!file) return; try{ const parsed=JSON.parse(await file.text()); if(!parsed.siteAliases) throw new Error('Missing siteAliases'); const next={...aliases}; for(const a of Object.values(parsed.siteAliases) as SiteAlias[]){ if(!a.hostname || !a.alias.includes('@')) throw new Error('Invalid alias data'); next[a.hostname]=a; } await browser.storage.local.set({backup_before_import:await loadAliasData(), siteAliases:next, settings:{...settings,...parsed.settings}}); setAliases(next); showToast('Imported JSON'); }catch(e:any){ showToast(`Import failed: ${e.message}`); } }; input.click(); }; + const download=(name:string, content:string, type:string)=>{ const url=URL.createObjectURL(new Blob([content],{type})); const a=document.createElement('a'); a.href=url; a.download=name; a.click(); URL.revokeObjectURL(url); }; + const rows=Object.values(aliases).filter(a=>(statusFilter==='all'||a.status===statusFilter)&&(categoryFilter==='all'||a.category===categoryFilter)&&`${a.hostname} ${a.alias} ${a.note}`.toLowerCase().includes(search.toLowerCase())).sort((a,b)=> sort==='useCount' ? b.useCount-a.useCount : String(b[sort]||'').localeCompare(String(a[sort]||''))); + + if(view==='dashboard') return

Alias dashboard

setSearch(e.target.value)}/>
{rows.length===0?

No aliases saved yet. Create your first website alias from the popup.

:{rows.map(a=>)}
WebsiteAliasCategoryStatusLast usedActions
{a.hostname}{a.alias}{a.category}{a.status}{a.lastUsedAt?.slice(0,10)||'—'}
}
; + if(view==='settings') return

Settings