feat(apk): notes de commandes, une commande copiee a la fois

Bloc Commands en tete d'une note "commands", groupe par groupe, avec un
bouton copier. ParseCommandNote est le portage du decoupage du poste
(groupes, lignes continuees). La copie passe par un rang, comme les autres
lignes du detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-10-10 19:39:38 +01:00
parent f112de508b
commit b1c0c1e11b
4 changed files with 118 additions and 0 deletions
+71
View File
@@ -37,6 +37,19 @@ function VaultToJson(AVault: TVault): string;
/// Les lignes d'une entree, dans l'ordre d'affichage. /// Les lignes d'une entree, dans l'ordre d'affichage.
function DetailToJson(const AEntry: TVaultEntry): string; function DetailToJson(const AEntry: TVaultEntry): string;
type
TCommandItem = record
Group, Cmd: string;
end;
/// NOTE DE COMMANDES (modele "commands"), meme regle que le poste
/// (js/app.js parseCommandNote) : "# titre" ouvre un groupe, chaque autre ligne
/// non vide est une commande ; une ligne finie par \, ` ou ^ se recolle a la
/// suivante, sur UNE ligne. Une ligne vide ou la fin du texte termine la
/// commande en cours.
function ParseCommandNote(const AText: string): TArray<TCommandItem>;
function IsCommandNote(const AEntry: TVaultEntry): Boolean;
/// Une ligne du detail par son rang, dans le MEME ordre que DetailToJson : la /// Une ligne du detail par son rang, dans le MEME ordre que DetailToJson : la
/// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui. /// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui.
/// Faux si le rang ne designe rien. /// Faux si le rang ne designe rien.
@@ -245,6 +258,49 @@ begin
Result := LRows; Result := LRows;
end; end;
function IsCommandNote(const AEntry: TVaultEntry): Boolean;
begin
Result := AEntry.IsNote and SameText(AEntry.Template, 'commands');
end;
function ParseCommandNote(const AText: string): TArray<TCommandItem>;
var
LGroup, LPending, LLine, LPart: string;
LCont: Boolean;
procedure Flush;
begin
if LPending <> '' then
begin
SetLength(Result, Length(Result) + 1);
Result[High(Result)].Group := LGroup;
Result[High(Result)].Cmd := LPending;
end;
LPending := '';
end;
begin
Result := nil;
LGroup := '';
LPending := '';
for LLine in AText.Replace(#13, '').Split([#10]) do
begin
var LTrim := LLine.Trim;
if LTrim = '' then begin Flush; Continue; end;
if (LPending = '') and LTrim.StartsWith('#') then
begin
LGroup := LTrim.TrimLeft(['#']).Trim;
Continue;
end;
LCont := CharInSet(LTrim[High(LTrim)], ['\', '`', '^']);
if LCont then LPart := Copy(LTrim, 1, Length(LTrim) - 1).TrimRight
else LPart := LTrim;
if LPending = '' then LPending := LPart else LPending := LPending + ' ' + LPart;
if not LCont then Flush;
end;
Flush;
end;
function DetailToJson(const AEntry: TVaultEntry): string; function DetailToJson(const AEntry: TVaultEntry): string;
var var
LRoot: TJSONObject; LRoot: TJSONObject;
@@ -278,6 +334,21 @@ begin
if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True)); if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True));
LArr.AddElement(LObj); LArr.AddElement(LObj);
end; end;
// La liste des commandes, pour l'affichage. La copie passe par un rang
// (copy?f=cmd&k=), comme les lignes : Delphi relit le clair chez lui.
if IsCommandNote(AEntry) then
begin
var LCmds := ParseCommandNote(AEntry.Password);
var LCArr := TJSONArray.Create;
LRoot.AddPair('cmds', LCArr);
for I := 0 to High(LCmds) do
begin
LObj := TJSONObject.Create;
LObj.AddPair('g', LCmds[I].Group);
LObj.AddPair('c', LCmds[I].Cmd);
LCArr.AddElement(LObj);
end;
end;
Result := LRoot.ToJSON; Result := LRoot.ToJSON;
finally finally
LRoot.Free; LRoot.Free;
+16
View File
@@ -447,6 +447,22 @@ l'ouverture, écriture 204, puis synchro du poste qui reprend la modification.
Portmaster (entrant autorisé pour Python) — sans le second, le port restait Portmaster (entrant autorisé pour Python) — sans le second, le port restait
fermé malgré la règle. fermé malgré la règle.
## Notes de commandes — 2026-10-10
Une note marquée **Commands** sur le poste (modèle `commands`) affiche en tête
un bloc *Commands* : une commande par ligne sous le titre de son groupe, avec
un bouton copier. Le texte entier reste dessous, dans *Content*.
- **Même découpage que le poste** : `ParseCommandNote` (PMA.WebUI) est le
portage de `parseCommandNote` (js/app.js) — `# titre` ouvre un groupe, une
ligne finie par `\`, `` ` `` ou `^` se recolle à la suivante. Les deux
doivent dire la même chose ; éprouvé sur les mêmes cas.
- **La copie passe par un rang** (`copy?f=cmd&k=`), comme les autres lignes :
Delphi relit la note et copie la commande.
- **Ni remplissage ni masquage** ici : pas de fenêtre cible, et le corps des
notes est déjà affiché en clair. Une note devient « Commands » depuis le
poste (case *Command list*) ; la synchro porte le modèle.
## Saisie : ce qui a résisté ## Saisie : ce qui a résisté
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne - **`user-select: none` du `body`** était hérité par les champs : le curseur ne
+23
View File
@@ -279,6 +279,11 @@
.field .val.multi { white-space: pre-wrap; word-break: normal; .field .val.multi { white-space: pre-wrap; word-break: normal;
overflow-wrap: anywhere; } overflow-wrap: anywhere; }
.field.totp .val { font-size: 30px; font-weight: 300; letter-spacing: 2px; } .field.totp .val { font-size: 30px; font-weight: 300; letter-spacing: 2px; }
/* Note de commandes : une commande par ligne, sous le titre de son groupe. */
.field.cmds .cmd-group { font-size: 12px; font-weight: 600; opacity: .7;
text-transform: uppercase; margin: 10px 0 2px; }
.field.cmds .cmd-line { padding: 4px 0; }
.field.cmds .val.cmd { font-family: monospace; font-size: 14px; }
.bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,.18); .bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,.18);
margin-top: 10px; overflow: hidden; } margin-top: 10px; overflow: hidden; }
.bar i { display: block; height: 100%; background: var(--text); .bar i { display: block; height: 100%; background: var(--text);
@@ -1089,6 +1094,23 @@ function renderDetail(d) {
+ '<button class="btn ico-btn" data-copy="totp">' + '<button class="btn ico-btn" data-copy="totp">'
+ ico('copy', 'Copy code') + '</button></div>' + ico('copy', 'Copy code') + '</button></div>'
+ '<div class="bar"><i id="totpb"></i></div></div>'; + '<div class="bar"><i id="totpb"></i></div></div>';
// NOTE DE COMMANDES : une ligne par commande, groupees sous leur titre, avec
// un bouton copier. Le texte entier reste plus bas, dans "Content".
if (d.cmds && d.cmds.length) {
var lastG = null;
html += '<div class="field cmds"><div class="cap">Commands</div>';
for (i = 0; i < d.cmds.length; i++) {
var cm = d.cmds[i];
if (cm.g !== lastG) {
lastG = cm.g;
if (cm.g) html += '<div class="cmd-group">' + esc(cm.g) + '</div>';
}
html += '<div class="line cmd-line"><div class="val cmd">' + esc(cm.c) + '</div>'
+ '<button class="btn ico-btn" data-copy="cmd" data-k="' + i + '">'
+ ico('copy', 'Copy command') + '</button></div>';
}
html += '</div>';
}
for (i = 0; i < detailRows.length; i++) { for (i = 0; i < detailRows.length; i++) {
var r = detailRows[i]; var r = detailRows[i];
html += '<div class="field"><div class="cap">' + esc(r.c) + '</div>' html += '<div class="field"><div class="cap">' + esc(r.c) + '</div>'
@@ -1170,6 +1192,7 @@ document.getElementById('fields').addEventListener('click', function (ev) {
var copy = t.getAttribute('data-copy'); var copy = t.getAttribute('data-copy');
if (copy === 'totp') { send('copy?f=totp'); return; } if (copy === 'totp') { send('copy?f=totp'); return; }
if (copy === 'row') { send('copy?f=row&k=' + t.getAttribute('data-k')); return; } if (copy === 'row') { send('copy?f=row&k=' + t.getAttribute('data-k')); return; }
if (copy === 'cmd') { send('copy?f=cmd&k=' + t.getAttribute('data-k')); return; }
}); });
+8
View File
@@ -2124,6 +2124,14 @@ begin
if DetailRowAt(FCurrent, StrToIntDef(Param('k'), -1), LCaption, LValue) then if DetailRowAt(FCurrent, StrToIntDef(Param('k'), -1), LCaption, LValue) then
CopyToClipboard(LValue, LCaption); CopyToClipboard(LValue, LCaption);
end end
// Une commande d'une note de commandes, par son rang dans la liste.
else if Param('f') = 'cmd' then
begin
var LCmds := ParseCommandNote(FCurrent.Password);
var LK := StrToIntDef(Param('k'), -1);
if (LK >= 0) and (LK <= High(LCmds)) then
CopyToClipboard(LCmds[LK].Cmd, 'Command');
end
else else
begin begin
LIdx := StrToIntDef(Param('i'), -1); LIdx := StrToIntDef(Param('i'), -1);