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) {