/// Entry detail view — shows fields, TOTP countdown, copy/fill shortcuts. import { getState, setState, sendMessage, navigate, escapeHtml } from '../popup'; import type { ManifestEntry } from '../../shared/types'; let totpInterval: ReturnType | null = null; function stopTotpTimer(): void { if (totpInterval !== null) { clearInterval(totpInterval); totpInterval = null; } } async function copyToClipboard(text: string): Promise { try { await navigator.clipboard.writeText(text); } catch { // Fallback for older browsers. const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } } export function renderEntryDetail(app: HTMLElement): void { const state = getState(); const entry = state.selectedEntry; const id = state.selectedId; if (!entry || !id) { navigate('list'); return; } stopTotpTimer(); let html = `
${escapeHtml(entry.name)}
`; // URL if (entry.url) { html += `
url
${escapeHtml(entry.url)}
`; } // Username if (entry.username) { html += `
username
${escapeHtml(entry.username)}
`; } // Password (masked by default) html += `
password
********
`; // TOTP if (entry.totp_secret) { html += `
totp
------
`; } // Notes if (entry.notes) { html += `
notes
${escapeHtml(entry.notes)}
`; } // Group if (entry.group) { html += `
group
${escapeHtml(entry.group)}
`; } // Metadata html += `
updated ${escapeHtml(entry.updated_at)}
`; // Key hints html += `
c copy user p copy pass ${entry.totp_secret ? 't copy totp' : ''} f autofill e edit d delete
`; app.innerHTML = html; // --- Password toggle --- let passwordVisible = false; const passwordDisplay = document.getElementById('password-display')!; const passwordVal = document.getElementById('password-val')!; passwordVal?.addEventListener('click', () => { passwordVisible = !passwordVisible; passwordDisplay.textContent = passwordVisible ? entry.password : '********'; }); // --- Back button --- document.getElementById('back-btn')?.addEventListener('click', goBack); // --- TOTP timer --- if (entry.totp_secret) { refreshTotp(id); totpInterval = setInterval(() => refreshTotp(id), 1000); } // --- Keyboard shortcuts --- const handler = async (e: KeyboardEvent) => { // Ignore if typing in an input. if ((e.target as HTMLElement).tagName === 'INPUT') return; switch (e.key) { case 'Escape': document.removeEventListener('keydown', handler); goBack(); break; case 'c': if (entry.username) await copyToClipboard(entry.username); break; case 'p': await copyToClipboard(entry.password); break; case 't': if (entry.totp_secret) { const codeEl = document.getElementById('totp-code'); if (codeEl) await copyToClipboard(codeEl.textContent ?? ''); } break; case 'f': { const resp = await sendMessage({ type: 'fill_credentials', username: entry.username ?? '', password: entry.password, }); if (!resp.ok) setState({ error: resp.error }); break; } case 'e': document.removeEventListener('keydown', handler); stopTotpTimer(); navigate('edit'); break; case 'd': e.preventDefault(); showDeleteConfirm(id, entry.name, handler); break; } }; document.addEventListener('keydown', handler); } async function refreshTotp(id: string): Promise { const resp = await sendMessage({ type: 'get_totp', id }); if (resp.ok) { const data = resp.data as { code: string; remaining_seconds: number }; const codeEl = document.getElementById('totp-code'); const barEl = document.getElementById('totp-bar-fill'); if (codeEl) codeEl.textContent = data.code; if (barEl) barEl.style.width = `${(data.remaining_seconds / 30) * 100}%`; } } function goBack(): void { stopTotpTimer(); // Reload the entry list. sendMessage({ type: 'list_entries' }).then(resp => { if (resp.ok) { const data = resp.data as { entries: Array<[string, ManifestEntry]> }; navigate('list', { entries: data.entries, selectedId: null, selectedEntry: null, }); } }); } function showDeleteConfirm(id: string, name: string, parentHandler: (e: KeyboardEvent) => void): void { const overlay = document.createElement('div'); overlay.className = 'confirm-overlay'; overlay.innerHTML = `

Delete ${escapeHtml(name)}?

`; document.body.appendChild(overlay); document.getElementById('cancel-delete')?.addEventListener('click', () => { overlay.remove(); }); document.getElementById('confirm-delete')?.addEventListener('click', async () => { overlay.remove(); setState({ loading: true }); const resp = await sendMessage({ type: 'delete_entry', id }); if (resp.ok) { document.removeEventListener('keydown', parentHandler); stopTotpTimer(); goBack(); } else { setState({ loading: false, error: resp.error }); } }); }