feat(quick-search): interrupteur Copy / Fill quand une fenetre cible existe

Ouverte par Ctrl+Shift+Q ou la barrette, la recherche remplissait toujours ;
copier restait impossible alors que le remplissage echoue vers une application
lancee en administrateur, ou quand le gestionnaire de Chrome s'en mele. Sans
cible (tray, palette), l'interrupteur n'apparait pas : remplir n'aurait rien a
viser. Le choix est retenu en pref DPAPI, les verbes des boutons et l'aide
suivent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-28 12:07:57 +01:00
parent 5e4fcfad0f
commit 7c26716831
3 changed files with 61 additions and 9 deletions
+10
View File
@@ -2133,6 +2133,16 @@ 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;
} }
.qs-mode {
display: flex; gap: 2px; padding: 2px; flex-shrink: 0;
background: var(--bg-elev-2); border-radius: 999px;
}
.qs-mode button {
border: 0; background: none; cursor: pointer;
color: var(--text-dim); font-size: 12px;
padding: 4px 10px; border-radius: 999px;
}
.qs-mode button.is-on { background: var(--accent); color: #fff; }
.quick-search-acts { .quick-search-acts {
display: flex; gap: 2px; flex-shrink: 0; display: flex; gap: 2px; flex-shrink: 0;
} }
+6
View File
@@ -1231,6 +1231,12 @@
<input id="quickSearchInput" type="text" <input id="quickSearchInput" type="text"
placeholder="Search and press Enter to copy password…" placeholder="Search and press Enter to copy password…"
autocomplete="off"> autocomplete="off">
<!-- Copier ou remplir : visible seulement quand une fenetre cible
existe - sans elle, remplir n'aurait rien a viser. -->
<div class="qs-mode" id="qsMode" hidden>
<button type="button" data-qsmode="copy">Copy</button>
<button type="button" data-qsmode="fill">Fill</button>
</div>
<kbd>Esc</kbd> <kbd>Esc</kbd>
</div> </div>
<div class="quick-search-results" id="quickSearchResults"></div> <div class="quick-search-results" id="quickSearchResults"></div>
+45 -9
View File
@@ -27,6 +27,39 @@ let quickSearchHideAfter = false;
// HWND and is waiting for cmd://autofill/execute. On pick we SendInput // HWND and is waiting for cmd://autofill/execute. On pick we SendInput
// the password instead of copying to the clipboard. // the password instead of copying to the clipboard.
let quickSearchFillMode = false; let quickSearchFillMode = false;
// CE QUE L'UTILISATEUR PREFERE quand une cible existe : remplir, ou copier
// quand meme - le remplissage echoue vers une application lancee en
// administrateur, ou quand le gestionnaire de Chrome s'en mele. Retenu en pref
// DPAPI : localStorage se vide a chaque lancement (port ephemere).
let quickSearchPreferCopy = false;
/// Remplit-on vraiment ? Il faut une cible ET ne pas avoir choisi Copy.
function qsFills() { return quickSearchFillMode && !quickSearchPreferCopy; }
function qsApplyMode() {
const box = document.getElementById('qsMode');
if (box) {
box.hidden = !quickSearchFillMode;
box.querySelectorAll('[data-qsmode]').forEach(b =>
b.classList.toggle('is-on', (b.dataset.qsmode === 'fill') === qsFills()));
}
const hintEl = document.querySelector('#quickSearchModal .quick-search-hint');
if (hintEl) {
hintEl.textContent = qsFills()
? 'Enter / click = password · Shift+Enter / right-click = user+password · Ctrl+Enter / Ctrl+click = username · Esc = cancel'
: 'Enter / click = copy password · Ctrl+Enter / Ctrl+click = copy username · Esc = close';
}
}
document.addEventListener('click', ev => {
const b = ev.target.closest && ev.target.closest('[data-qsmode]');
if (!b) return;
quickSearchPreferCopy = b.dataset.qsmode === 'copy';
if (Bridge.active) Bridge.setPref('qsCopy', quickSearchPreferCopy ? '1' : '0');
qsApplyMode();
quickSearchRender();
document.getElementById('quickSearchInput').focus();
});
function quickSearchScoreEntry(e, q) { function quickSearchScoreEntry(e, q) {
if (!q) return 1; // empty query → all entries pass, ordering preserved if (!q) return 1; // empty query → all entries pass, ordering preserved
@@ -89,7 +122,7 @@ function quickSearchRender() {
row.appendChild(avatar); row.appendChild(avatar);
row.appendChild(main); row.appendChild(main);
// Un bouton par valeur presente : pas besoin de retenir clic / Ctrl+clic. // Un bouton par valeur presente : pas besoin de retenir clic / Ctrl+clic.
const verb = quickSearchFillMode ? 'Fill ' : 'Copy '; const verb = qsFills() ? 'Fill ' : 'Copy ';
const acts = el('div', { class: 'quick-search-acts' }); const acts = el('div', { class: 'quick-search-acts' });
const act = (ic, title, mode) => { const act = (ic, title, mode) => {
const b = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', title }); const b = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', title });
@@ -133,7 +166,7 @@ async function quickSearchPickEntry(entry, mode) {
try { if (secret && secret !== '[ERROR]') code = (await generateTOTP(secret)).code; } try { if (secret && secret !== '[ERROR]') code = (await generateTOTP(secret)).code; }
catch (_) { code = ''; } catch (_) { code = ''; }
if (!code) { toast('2FA: cannot read the secret', 'error'); return; } if (!code) { toast('2FA: cannot read the secret', 'error'); return; }
if (quickSearchFillMode) { if (qsFills()) {
autofillPendingToast = entryDisplayName(entry) + ' · 2FA code sent'; autofillPendingToast = entryDisplayName(entry) + ' · 2FA code sent';
if (Bridge.active) Bridge.executeAutofill('', code, quickSearchHideAfter); if (Bridge.active) Bridge.executeAutofill('', code, quickSearchHideAfter);
quickSearchFillMode = false; quickSearchFillMode = false;
@@ -159,7 +192,7 @@ async function quickSearchPickEntry(entry, mode) {
// 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 (qsFills()) {
if (mode === 'user') { if (mode === 'user') {
const u = entry.username || ''; const u = entry.username || '';
if (!u) { toast('No username on this entry', 'warning'); return; } if (!u) { toast('No username on this entry', 'warning'); return; }
@@ -527,12 +560,15 @@ function openQuickSearchModal(hideAfter, forFill) {
quickSearchSelected = 0; quickSearchSelected = 0;
quickSearchHideAfter = !!hideAfter; quickSearchHideAfter = !!hideAfter;
quickSearchFillMode = !!forFill; quickSearchFillMode = !!forFill;
// Subtle hint to the user about what Enter will do. qsApplyMode();
const hintEl = modal.querySelector('.quick-search-hint'); // La preference arrive de Delphi : on affiche tout de suite, et on
if (hintEl) { // rectifie des qu'elle est la (une fraction de seconde).
hintEl.textContent = forFill if (forFill && Bridge.active) {
? 'Enter / click = password · Shift+Enter / right-click = user+password · Ctrl+Enter / Ctrl+click = username · Esc = cancel' Bridge.getPref('qsCopy').then(v => {
: 'Enter / click = copy password · Ctrl+Enter / Ctrl+click = copy username · Esc = close'; quickSearchPreferCopy = v === '1';
qsApplyMode();
quickSearchRender();
}).catch(() => {});
} }
quickSearchRender(); quickSearchRender();
setTimeout(() => input.focus(), 50); setTimeout(() => input.focus(), 50);