feat(quick-search): boutons par entree - utilisateur, mot de passe ou note, code 2FA (s'ils existent)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-18 23:15:49 +01:00
parent cfa2a798e5
commit 652815ea98
3 changed files with 53 additions and 0 deletions
+3
View File
@@ -2133,6 +2133,9 @@ input[type="range"]::-webkit-slider-thumb {
color: var(--text); color: var(--text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
} }
.quick-search-acts {
display: flex; gap: 2px; flex-shrink: 0;
}
.quick-search-sub { .quick-search-sub {
font-size: 12px; font-size: 12px;
color: var(--text-dim); color: var(--text-dim);
+2
View File
@@ -47,6 +47,8 @@
<symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></symbol> <symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></symbol>
<symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></symbol> <symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></symbol>
<symbol id="i-paperclip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol> <symbol id="i-paperclip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/></symbol>
<symbol id="i-note" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="14 3 14 9 20 9"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></symbol>
<symbol id="i-qr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><path d="M14 14h3v3h-3zM21 14v.01M14 21h.01M17 21h4v-4"/></symbol> <symbol id="i-qr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><path d="M14 14h3v3h-3zM21 14v.01M14 21h.01M17 21h4v-4"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol> <symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
<symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none"> <symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none">
+48
View File
@@ -88,6 +88,21 @@ function quickSearchRender() {
main.appendChild(el('div', { class: 'quick-search-sub' }, e.username)); main.appendChild(el('div', { class: 'quick-search-sub' }, e.username));
row.appendChild(avatar); row.appendChild(avatar);
row.appendChild(main); 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 // Left click → password only; Ctrl+click → username only
// (step-2 forms / unlock screens). // (step-2 forms / unlock screens).
row.addEventListener('click', ev => row.addEventListener('click', ev =>
@@ -106,9 +121,42 @@ function quickSearchRender() {
// mode: 'full' (user + Tab + password), 'user' (username only) or 'pwd' // mode: 'full' (user + Tab + password), 'user' (username only) or 'pwd'
// (password only). In fill mode each maps to a SendInput variant; in copy // (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. // 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) { async function quickSearchPickEntry(entry, mode) {
mode = mode || 'full'; 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 // Fill mode (Ctrl+Shift+Q hotkey): SendInput directly into the HWND
// Delphi saved when the hotkey fired. No clipboard touch. // Delphi saved when the hotkey fired. No clipboard touch.
if (quickSearchFillMode) { if (quickSearchFillMode) {