feat(notes): notes de commandes, une commande copiee ou tapee a la fois
Modele "commands" : "# titre" ouvre un groupe, chaque ligne est une commande. Le corps reste du texte (aucune colonne, sync et APK inchanges). Panneau : liste avec Copier. Recherche rapide : la note s'ouvre sur sa liste filtrable ; Entree tape la commande sans Entree et sans vider le champ, Ctrl+Entree copie, Esc revient. Decoupage teste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+98
-2
@@ -33,6 +33,87 @@ let quickSearchFillMode = false;
|
||||
// DPAPI : localStorage se vide a chaque lancement (port ephemere).
|
||||
let quickSearchPreferCopy = false;
|
||||
|
||||
// NOTE DE COMMANDES ouverte dans la recherche rapide : la liste remplace les
|
||||
// entrees, la saisie la filtre, Esc revient aux entrees. null = liste normale.
|
||||
let qsCommands = null;
|
||||
|
||||
async function qsEnterCommands(entry) {
|
||||
const body = await decryptPwd(entry.encrypted_password, entry.iv);
|
||||
if (body === '[ERROR]') { toast('Decryption error', 'error'); return; }
|
||||
qsCommands = parseCommandNote(body);
|
||||
const input = document.getElementById('quickSearchInput');
|
||||
input.value = '';
|
||||
input.placeholder = entryDisplayName(entry) + ' - filter commands… (Esc = back)';
|
||||
quickSearchSelected = 0;
|
||||
qsApplyMode();
|
||||
quickSearchRender();
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function qsLeaveCommands() {
|
||||
qsCommands = null;
|
||||
const input = document.getElementById('quickSearchInput');
|
||||
input.value = '';
|
||||
input.placeholder = 'Search and press Enter to copy password…';
|
||||
quickSearchSelected = 0;
|
||||
qsApplyMode();
|
||||
quickSearchRender();
|
||||
}
|
||||
|
||||
// Une commande, une ligne, SANS Entree : l'utilisateur valide lui-meme. Ni
|
||||
// Ctrl+A ni Suppr avant (noClear) - dans un terminal Ctrl+A ramene en debut de
|
||||
// ligne, ce ne serait pas "tout selectionner".
|
||||
async function qsPickCommand(cmd, forceCopy) {
|
||||
if (qsFills() && !forceCopy) {
|
||||
autofillPendingToast = 'Command sent';
|
||||
if (Bridge.active) Bridge.executeAutofill('', cmd, quickSearchHideAfter, undefined, true);
|
||||
quickSearchFillMode = false;
|
||||
quickSearchHideAfter = false;
|
||||
qsCommands = null;
|
||||
closeQuickSearchModal();
|
||||
return;
|
||||
}
|
||||
if (Bridge.active) Bridge.copySecure(cmd, 30000);
|
||||
else { try { await navigator.clipboard.writeText(cmd); } catch (_) {} }
|
||||
toast('Command copied');
|
||||
qsCommands = null;
|
||||
closeQuickSearchModal(true);
|
||||
}
|
||||
|
||||
function qsRenderCommands(box, q) {
|
||||
const ql = q.toLowerCase();
|
||||
const list = qsCommands
|
||||
.map((c, i) => ({ c, i }))
|
||||
.filter(x => !ql || x.c.cmd.toLowerCase().includes(ql) || x.c.group.toLowerCase().includes(ql));
|
||||
if (!list.length) {
|
||||
box.appendChild(el('div', { class: 'quick-search-empty' },
|
||||
q ? 'No command matches "' + q + '"' : 'No command in this note'));
|
||||
quickSearchSelected = 0;
|
||||
return;
|
||||
}
|
||||
if (quickSearchSelected >= list.length) quickSearchSelected = 0;
|
||||
if (quickSearchSelected < 0) quickSearchSelected = list.length - 1;
|
||||
list.forEach((x, n) => {
|
||||
const row = el('div', {
|
||||
class: 'quick-search-row' + (n === quickSearchSelected ? ' is-selected' : ''),
|
||||
'data-cmd': String(x.i),
|
||||
});
|
||||
const main = el('div', { class: 'quick-search-main' });
|
||||
main.appendChild(el('div', { class: 'quick-search-name qs-cmd' }, x.c.cmd));
|
||||
if (x.c.group) main.appendChild(el('div', { class: 'quick-search-sub' }, x.c.group));
|
||||
row.appendChild(main);
|
||||
const acts = el('div', { class: 'quick-search-acts' });
|
||||
const b = el('button', { class: 'icon-btn icon-btn-sm', type: 'button',
|
||||
title: qsFills() ? 'Copy instead of typing' : 'Copy command' });
|
||||
b.appendChild(icon('i-copy'));
|
||||
b.addEventListener('click', ev => { ev.stopPropagation(); qsPickCommand(x.c.cmd, true); });
|
||||
acts.appendChild(b);
|
||||
row.appendChild(acts);
|
||||
row.addEventListener('click', ev => qsPickCommand(x.c.cmd, ev.ctrlKey || ev.metaKey));
|
||||
box.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
/// Remplit-on vraiment ? Il faut une cible ET ne pas avoir choisi Copy.
|
||||
function qsFills() { return quickSearchFillMode && !quickSearchPreferCopy; }
|
||||
|
||||
@@ -44,7 +125,11 @@ function qsApplyMode() {
|
||||
b.classList.toggle('is-on', (b.dataset.qsmode === 'fill') === qsFills()));
|
||||
}
|
||||
const hintEl = document.querySelector('#quickSearchModal .quick-search-hint');
|
||||
if (hintEl) {
|
||||
if (hintEl && qsCommands) {
|
||||
hintEl.textContent = qsFills()
|
||||
? 'Enter / click = type the command (no Enter sent) · Ctrl+Enter / Ctrl+click = copy · Esc = back'
|
||||
: 'Enter / click = copy the command · Esc = back';
|
||||
} else 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';
|
||||
@@ -80,6 +165,12 @@ function quickSearchScoreEntry(e, q) {
|
||||
|
||||
function quickSearchRender() {
|
||||
const q = document.getElementById('quickSearchInput').value.trim();
|
||||
if (qsCommands) {
|
||||
const box = document.getElementById('quickSearchResults');
|
||||
box.innerHTML = '';
|
||||
qsRenderCommands(box, q);
|
||||
return;
|
||||
}
|
||||
const list = state.entries
|
||||
.map(e => ({ e, s: quickSearchScoreEntry(e, q) }))
|
||||
.filter(x => x.s > 0)
|
||||
@@ -134,7 +225,9 @@ function quickSearchRender() {
|
||||
if (e.username) act('i-user', verb + 'username', 'user');
|
||||
// UNE NOTE SE COPIE TOUJOURS, meme en Fill : tapee, chacun de ses
|
||||
// retours a la ligne partirait comme un Entree dans le formulaire.
|
||||
if (e.kind === 'note')
|
||||
if (e.template === 'commands')
|
||||
act('i-note', 'Open the command list', 'cmds');
|
||||
else if (e.kind === 'note')
|
||||
act('i-note', qsFills() ? 'Copy note (a note is never typed)' : 'Copy note', 'note');
|
||||
else act('i-key', verb + 'password', 'pwd');
|
||||
if (e.totp_secret && e.totp_iv) act('i-clock', verb + '2FA code', 'totp');
|
||||
@@ -162,6 +255,8 @@ function quickSearchRender() {
|
||||
// comme des Entree dans le formulaire cible.
|
||||
async function quickSearchPickEntry(entry, mode) {
|
||||
mode = mode || 'full';
|
||||
// Note de commandes : on choisit UNE commande, jamais la note entiere.
|
||||
if (entry.template === 'commands' && mode !== 'user') return qsEnterCommands(entry);
|
||||
|
||||
if (mode === 'totp') {
|
||||
const secret = await decryptTotpSecret(entry.totp_secret, entry.totp_iv);
|
||||
@@ -559,6 +654,7 @@ function openQuickSearchModal(hideAfter, forFill) {
|
||||
const modal = document.getElementById('quickSearchModal');
|
||||
const input = document.getElementById('quickSearchInput');
|
||||
modal.classList.remove('is-hidden');
|
||||
if (qsCommands) qsLeaveCommands();
|
||||
input.value = '';
|
||||
quickSearchSelected = 0;
|
||||
quickSearchHideAfter = !!hideAfter;
|
||||
|
||||
Reference in New Issue
Block a user