diff --git a/css/style.css b/css/style.css index 7e81366..c624ef6 100644 --- a/css/style.css +++ b/css/style.css @@ -2133,6 +2133,9 @@ input[type="range"]::-webkit-slider-thumb { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.quick-search-acts { + display: flex; gap: 2px; flex-shrink: 0; +} .quick-search-sub { font-size: 12px; color: var(--text-dim); diff --git a/index.html b/index.html index 2dfc81d..a5655eb 100644 --- a/index.html +++ b/index.html @@ -47,6 +47,8 @@ + + diff --git a/js/app.overlays.js b/js/app.overlays.js index e9d1c88..981bc26 100644 --- a/js/app.overlays.js +++ b/js/app.overlays.js @@ -88,6 +88,21 @@ function quickSearchRender() { main.appendChild(el('div', { class: 'quick-search-sub' }, e.username)); row.appendChild(avatar); row.appendChild(main); + // Un bouton par valeur presente : pas besoin de retenir clic / Ctrl+clic. + const verb = quickSearchFillMode ? 'Fill ' : 'Copy '; + const acts = el('div', { class: 'quick-search-acts' }); + const act = (ic, title, mode) => { + const b = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', title }); + b.appendChild(icon(ic)); + b.addEventListener('click', ev => { ev.stopPropagation(); quickSearchPickEntry(e, mode); }); + b.addEventListener('contextmenu', ev => ev.stopPropagation()); + acts.appendChild(b); + }; + if (e.username) act('i-user', verb + 'username', 'user'); + if (e.kind === 'note') act('i-note', 'Copy note', 'note'); + else act('i-key', verb + 'password', 'pwd'); + if (e.totp_secret && e.totp_iv) act('i-clock', verb + '2FA code', 'totp'); + row.appendChild(acts); // Left click → password only; Ctrl+click → username only // (step-2 forms / unlock screens). row.addEventListener('click', ev => @@ -106,9 +121,42 @@ function quickSearchRender() { // mode: 'full' (user + Tab + password), 'user' (username only) or 'pwd' // (password only). In fill mode each maps to a SendInput variant; in copy // mode 'full' has no meaning so it falls back to copying the password. +// 'totp' = code 2FA courant (tape comme un mot de passe, ou copie) ; 'note' = +// corps d'une note, toujours COPIE - le taper enverrait ses retours a la ligne +// comme des Entree dans le formulaire cible. async function quickSearchPickEntry(entry, mode) { mode = mode || 'full'; + if (mode === 'totp') { + const secret = await decryptTotpSecret(entry.totp_secret, entry.totp_iv); + let code = ''; + try { if (secret && secret !== '[ERROR]') code = (await generateTOTP(secret)).code; } + catch (_) { code = ''; } + if (!code) { toast('2FA: cannot read the secret', 'error'); return; } + if (quickSearchFillMode) { + autofillPendingToast = entryDisplayName(entry) + ' · 2FA code sent'; + if (Bridge.active) Bridge.executeAutofill('', code, quickSearchHideAfter); + quickSearchFillMode = false; + quickSearchHideAfter = false; + closeQuickSearchModal(); + return; + } + if (Bridge.active) Bridge.copySecure(code, 30000); + else { try { await navigator.clipboard.writeText(code); } catch (_) {} } + toast(entryDisplayName(entry) + ' · 2FA code copied'); + closeQuickSearchModal(true); + return; + } + if (mode === 'note') { + const body = await decryptPwd(entry.encrypted_password, entry.iv); + if (body === '[ERROR]') { toast('Decryption error', 'error'); return; } + if (Bridge.active) Bridge.copySecure(body, 30000); + else { try { await navigator.clipboard.writeText(body); } catch (_) {} } + toast(entryDisplayName(entry) + ' · note copied'); + closeQuickSearchModal(true); + return; + } + // Fill mode (Ctrl+Shift+Q hotkey): SendInput directly into the HWND // Delphi saved when the hotkey fired. No clipboard touch. if (quickSearchFillMode) {