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:
@@ -37,6 +37,19 @@ function VaultToJson(AVault: TVault): string;
|
||||
/// Les lignes d'une entree, dans l'ordre d'affichage.
|
||||
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
|
||||
/// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui.
|
||||
/// Faux si le rang ne designe rien.
|
||||
@@ -245,6 +258,49 @@ begin
|
||||
Result := LRows;
|
||||
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;
|
||||
var
|
||||
LRoot: TJSONObject;
|
||||
@@ -278,6 +334,21 @@ begin
|
||||
if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True));
|
||||
LArr.AddElement(LObj);
|
||||
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;
|
||||
finally
|
||||
LRoot.Free;
|
||||
|
||||
@@ -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
|
||||
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é
|
||||
|
||||
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne
|
||||
|
||||
@@ -279,6 +279,11 @@
|
||||
.field .val.multi { white-space: pre-wrap; word-break: normal;
|
||||
overflow-wrap: anywhere; }
|
||||
.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);
|
||||
margin-top: 10px; overflow: hidden; }
|
||||
.bar i { display: block; height: 100%; background: var(--text);
|
||||
@@ -1089,6 +1094,23 @@ function renderDetail(d) {
|
||||
+ '<button class="btn ico-btn" data-copy="totp">'
|
||||
+ ico('copy', 'Copy code') + '</button></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++) {
|
||||
var r = detailRows[i];
|
||||
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');
|
||||
if (copy === 'totp') { send('copy?f=totp'); 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; }
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -2124,6 +2124,14 @@ begin
|
||||
if DetailRowAt(FCurrent, StrToIntDef(Param('k'), -1), LCaption, LValue) then
|
||||
CopyToClipboard(LValue, LCaption);
|
||||
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
|
||||
begin
|
||||
LIdx := StrToIntDef(Param('i'), -1);
|
||||
|
||||
Reference in New Issue
Block a user