diff --git a/android-client/PMA.WebUI.pas b/android-client/PMA.WebUI.pas index d059699..f2dcc3d 100644 --- a/android-client/PMA.WebUI.pas +++ b/android-client/PMA.WebUI.pas @@ -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; +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; +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; diff --git a/android-client/README.md b/android-client/README.md index fdf58f1..3eedcb0 100644 --- a/android-client/README.md +++ b/android-client/README.md @@ -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 diff --git a/android-client/web/app.html b/android-client/web/app.html index 6e4f6ba..e143824 100644 --- a/android-client/web/app.html +++ b/android-client/web/app.html @@ -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) { + '' + '
'; + // 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 += '
Commands
'; + for (i = 0; i < d.cmds.length; i++) { + var cm = d.cmds[i]; + if (cm.g !== lastG) { + lastG = cm.g; + if (cm.g) html += '
' + esc(cm.g) + '
'; + } + html += '
' + esc(cm.c) + '
' + + '
'; + } + html += '
'; + } for (i = 0; i < detailRows.length; i++) { var r = detailRows[i]; html += '
' + esc(r.c) + '
' @@ -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; } }); diff --git a/mainApk.pas b/mainApk.pas index b8ee1ae..548da89 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -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);