feat(android): copy from the list, server params grouped, HTML probe

The card no longer swallows the tap: it covered the whole row and OnItemClick never fired. It paints, nothing more - HitTest false - and its children keep theirs. The two buttons take a Left and a Right rather than fixed coordinates, which put them outside the frame.

The sync password joins the URL and the token: it is a server parameter, and it filled the home screen although it is typed once. The panel opens by itself when one of the three is missing - folding away empty fields hides the only thing left to do.

And an HTML probe, to answer a question rather than argue it: the same list painted by the system engine. It renders well, Arabic included. Two lessons: FMX passes a nil encoding to loadDataWithBaseURL, and this source file carries no byte-order mark, so Delphi reads it as ANSI and an Arabic literal is already broken at compile time - hence the numeric references. Neither is a weakness of the engine, which is what I first suspected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-16 23:16:18 +01:00
parent e85f46f501
commit 7218df2f6f
+120 -11
View File
@@ -29,6 +29,7 @@ uses
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform,
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects, FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
FMX.WebBrowser,
PMA.Vault; PMA.Vault;
const const
@@ -100,7 +101,11 @@ type
// --- ecran 1 : deverrouillage // --- ecran 1 : deverrouillage
FLayUnlock: TLayout; FLayUnlock: TLayout;
FUrl, FToken, FPwd: TEdit; FUrl, FToken, FPwd: TEdit;
FBtnOpen, FBtnSync, FBtnSettings, FBtnLog: TRectangle; FBtnOpen, FBtnSync, FBtnSettings, FBtnLog, FBtnHtml: TRectangle;
/// Sondage HTML, cree au premier appui seulement : un moteur de rendu
/// entier ne se paie pas au lancement pour un essai.
FLayWeb: TLayout;
FWeb: TWebBrowser;
FLaySettings: TLayout; FLaySettings: TLayout;
FStatus: TLabel; FStatus: TLabel;
FLog: TMemo; FLog: TMemo;
@@ -197,6 +202,8 @@ type
procedure OnBackTap(Sender: TObject); procedure OnBackTap(Sender: TObject);
procedure OnToggleSettings(Sender: TObject); procedure OnToggleSettings(Sender: TObject);
procedure OnToggleLog(Sender: TObject); procedure OnToggleLog(Sender: TObject);
procedure OnHtmlTest(Sender: TObject);
procedure OnHtmlClose(Sender: TObject);
procedure OnClipTimer(Sender: TObject); procedure OnClipTimer(Sender: TObject);
procedure OnHardwareBack(Sender: TObject; var Key: Word; procedure OnHardwareBack(Sender: TObject; var Key: Word;
var KeyChar: WideChar; Shift: TShiftState); var KeyChar: WideChar; Shift: TShiftState);
@@ -350,10 +357,15 @@ begin
FLaySettings := TLayout.Create(Self); FLaySettings := TLayout.Create(Self);
FLaySettings.Parent := FLayUnlock; FLaySettings.Parent := FLayUnlock;
FLaySettings.Align := TAlignLayout.Top; FLaySettings.Align := TAlignLayout.Top;
FLaySettings.Height := 116; FLaySettings.Height := 174;
FLaySettings.Visible := False; // Les trois vont ensemble : ce sont les coordonnees du serveur, dont le mot
// de passe fait partie. Il occupait l'ecran d'accueil en permanence alors
// qu'on n'y touche qu'une fois. TopEdit empile du bas vers le haut, d'ou
// l'ordre inverse ici.
FPwd := TopEdit(FLaySettings, 'sync password', TEST_PWD, True);
FToken := TopEdit(FLaySettings, 'bearer token', FIRSTRUN_TOKEN); FToken := TopEdit(FLaySettings, 'bearer token', FIRSTRUN_TOKEN);
FUrl := TopEdit(FLaySettings, 'endpoint URL', FIRSTRUN_URL); FUrl := TopEdit(FLaySettings, 'endpoint URL', FIRSTRUN_URL);
FPwd.Margins.Rect := RectF(16, 8, 16, 0);
FToken.Margins.Rect := RectF(16, 8, 16, 0); FToken.Margins.Rect := RectF(16, 8, 16, 0);
FUrl.Margins.Rect := RectF(16, 8, 16, 0); FUrl.Margins.Rect := RectF(16, 8, 16, 0);
// Ce que l'appareil a retenu l'emporte sur les valeurs de premier lancement. // Ce que l'appareil a retenu l'emporte sur les valeurs de premier lancement.
@@ -362,6 +374,9 @@ begin
FUrl.Text := LUrl; FUrl.Text := LUrl;
if LToken <> '' then FToken.Text := LToken; if LToken <> '' then FToken.Text := LToken;
end; end;
// Replier des champs vides revient a cacher la seule chose a faire.
FLaySettings.Visible := (FUrl.Text.Trim = '') or (FToken.Text.Trim = '')
or (FPwd.Text = '');
// Deux commandes discretes cote a cote : elles ne servent qu'au reglage et // Deux commandes discretes cote a cote : elles ne servent qu'au reglage et
// au diagnostic, et n'ont pas a peser autant que les actions principales. // au diagnostic, et n'ont pas a peser autant que les actions principales.
@@ -379,14 +394,18 @@ begin
FBtnSettings := FlatButton(LRow, 'Server settings', OnToggleSettings); FBtnSettings := FlatButton(LRow, 'Server settings', OnToggleSettings);
FBtnSettings.Align := TAlignLayout.Client; FBtnSettings.Align := TAlignLayout.Client;
// Sondage, pas une fonction : il repond a une seule question, celle de
// savoir si une page HTML peinte par le moteur du systeme rend mieux que
// ces controles-ci. A retirer des que la reponse est prise.
FBtnHtml := FlatButton(LRow, 'HTML', OnHtmlTest);
FBtnHtml.Align := TAlignLayout.Left;
FBtnHtml.Width := 90;
FBtnHtml.Margins.Right := 8;
FBtnSync := TopButton(FLayUnlock, 'Sync', DoSync); FBtnSync := TopButton(FLayUnlock, 'Sync', DoSync);
// Accentue : c'est l'action attendue quand aucun coffre n'est encore la. // Accentue : c'est l'action attendue quand aucun coffre n'est encore la.
FBtnOpen := TopButton(FLayUnlock, 'Open vault', DoOpenLocal, 52, True); FBtnOpen := TopButton(FLayUnlock, 'Open vault', DoOpenLocal, 52, True);
// Pre-rempli depuis le fichier local, jamais depuis ce fichier-ci.
FPwd := TopEdit(FLayUnlock, 'sync password', TEST_PWD, True);
FPwd.Margins.Rect := RectF(16, 16, 16, 0);
FStatus := TLabel.Create(Self); FStatus := TLabel.Create(Self);
FStatus.Parent := FLayUnlock; FStatus.Parent := FLayUnlock;
FStatus.Align := TAlignLayout.Top; FStatus.Align := TAlignLayout.Top;
@@ -892,6 +911,89 @@ begin
FLog.Visible := not FLog.Visible; FLog.Visible := not FLog.Visible;
end; end;
/// Sondage HTML : la meme liste, les memes blocs, mais peints par le moteur du
/// systeme au lieu de FMX. Ce qu'il faut regarder : le rendu de l'arabe, la
/// fluidite du defilement, et si la page se fond dans le degrade ou flotte
/// dans un rectangle blanc. Contenu factice - rien du coffre ne passe ici.
procedure TForm8.OnHtmlTest(Sender: TObject);
const
PAGE =
// SANS LA DECLARATION DE JEU DE CARACTERES l'arabe sort en charabia : le
// moteur recoit de l'UTF-8 et le lit en latin-1 faute d'indication.
'<!doctype html><meta charset="utf-8">' +
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
'<style>' +
'html,body{margin:0;height:100%;background:linear-gradient(#3E6394,#1B3563);' +
'color:#fff;font:15px system-ui,sans-serif;-webkit-tap-highlight-color:transparent}' +
'.wrap{padding:16px}' +
'h1{font-size:19px;font-weight:600;margin:4px 0 14px}' +
'.s{display:flex;align-items:center;background:rgba(255,255,255,.10);' +
'border-radius:14px;padding:12px 14px;margin-bottom:14px;color:#E8F0FA}' +
'.row{display:flex;align-items:center;gap:10px;background:rgba(255,255,255,.12);' +
'border-radius:20px;padding:12px;margin-bottom:8px}' +
'.av{width:40px;height:40px;border-radius:20px;background:rgba(255,255,255,.18);' +
'display:flex;align-items:center;justify-content:center;font-weight:600}' +
'.t{flex:1;min-width:0}.t b{display:block;font-weight:500}' +
'.t span{color:#B3E8F0FA;font-size:13px}' +
'.b{background:rgba(255,255,255,.18);border:0;color:#fff;border-radius:14px;' +
'padding:10px 12px;font-size:13px}' +
'.b:active{opacity:.62}' +
'</style><div class=wrap><h1>Rendu HTML</h1>' +
'<div class=s>Rechercher</div>' +
'<div class=row><div class=av>G</div><div class=t><b>Gitea</b>' +
'<span>git.example.com</span></div><button class=b>User</button>' +
'<button class=b>Pass</button></div>' +
// L'ARABE EN REFERENCES NUMERIQUES, pas en clair : ce fichier n'a pas de
// marque d'ordre d'octets, Delphi le lit donc en ANSI et abimerait le
// litteral bien avant que le moteur de rendu le voie. Le detail affiche
// de l'arabe correct parce que celui-la vient du JSON, a l'execution.
'<div class=row><div class=av>&#1605;</div>' +
'<div class=t><b>&#1605;&#1585;&#1581;&#1576;&#1575; &#1576;&#1575;&#1604;&#1593;&#1575;&#1604;&#1605;</b>' +
'<span>&#1575;&#1582;&#1578;&#1576;&#1575;&#1585; &#1575;&#1604;&#1593;&#1585;&#1576;&#1610;&#1577;</span></div>' +
'<button class=b>User</button><button class=b>Pass</button></div>' +
'<div class=row><div class=av>N</div><div class=t><b>Nextcloud</b>' +
'<span>cloud.example.com</span></div><button class=b>User</button>' +
'<button class=b>Pass</button></div></div>';
var
LBar: TLayout;
LBtn: TRectangle;
begin
if FLayWeb = nil then
begin
FLayWeb := TLayout.Create(Self);
FLayWeb.Parent := Self;
FLayWeb.Align := TAlignLayout.Client;
// La barre est POSEE AVANT le navigateur et alignee en bas : sur Android
// le moteur est une vraie vue du systeme, qui se peint par-dessus tout ce
// que FMX dessine. Elle ne survivrait pas sous lui.
LBar := TLayout.Create(FLayWeb);
LBar.Parent := FLayWeb;
LBar.Align := TAlignLayout.Bottom;
LBar.Height := 60;
LBtn := FlatButton(LBar, 'Close', OnHtmlClose, True);
LBtn.Align := TAlignLayout.Client;
LBtn.Margins.Rect := RectF(16, 4, 16, 10);
FWeb := TWebBrowser.Create(Self);
FWeb.Parent := FLayWeb;
FWeb.Align := TAlignLayout.Client;
// La page est en ASCII pur, d'ou les references numeriques plus haut :
// FMX appelle loadDataWithBaseURL en laissant l'encodage a nil, et une
// URL « data: », qui le porterait, est refusee par Navigate.
// ponytail: sonde. Une vraie page serait servie en HTTP, comme sur le
// poste, et la question de l'encodage ne se poserait plus.
FWeb.LoadFromStrings(PAGE, '');
end;
FLayWeb.Visible := True;
FLayWeb.BringToFront;
end;
procedure TForm8.OnHtmlClose(Sender: TObject);
begin
FLayWeb.Visible := False;
end;
{ ---------------------------------------------------------------------------- { ----------------------------------------------------------------------------
Ouverture, synchronisation, verrouillage Ouverture, synchronisation, verrouillage
---------------------------------------------------------------------------- } ---------------------------------------------------------------------------- }
@@ -1486,6 +1588,10 @@ begin
LCard := Surface(LItem); LCard := Surface(LItem);
LCard.Align := TAlignLayout.Client; LCard.Align := TAlignLayout.Client;
LCard.Margins.Rect := RectF(4, 3, 4, 3); LCard.Margins.Rect := RectF(4, 3, 4, 3);
// SANS CECI L'ENTREE N'EST PLUS CLIQUABLE : le bloc couvre toute la
// ligne et absorbe le tap, donc OnItemClick ne part jamais. Il ne sert
// qu'a peindre. Ses enfants, eux, gardent leur propre HitTest.
LCard.HitTest := False;
// Copier sans ouvrir l'entree - le geste le plus frequent. Les deux // Copier sans ouvrir l'entree - le geste le plus frequent. Les deux
// boutons sont poses a la coordonnee dans leur propre conteneur : deux // boutons sont poses a la coordonnee dans leur propre conteneur : deux
@@ -1496,14 +1602,17 @@ begin
LActions.Width := 150; LActions.Width := 150;
LActions.Margins.Rect := RectF(0, 8, 8, 8); LActions.Margins.Rect := RectF(0, 8, 8, 8);
// Un Left et un Right, jamais deux fois le meme cote : leur ordre serait
// alors laisse au hasard. Ils prennent la hauteur du conteneur, ce qui
// evite de poser des coordonnees qui debordaient du bloc.
LBtnUser := FlatButton(LActions, 'User', OnRowCopyUser); LBtnUser := FlatButton(LActions, 'User', OnRowCopyUser);
LBtnUser.Align := TAlignLayout.None; LBtnUser.Align := TAlignLayout.Left;
LBtnUser.SetBounds(0, 0, 70, 54); LBtnUser.Width := 70;
LBtnUser.Tag := FFiltered[I]; LBtnUser.Tag := FFiltered[I];
LBtnPwd := FlatButton(LActions, 'Pass', OnRowCopyPwd); LBtnPwd := FlatButton(LActions, 'Pass', OnRowCopyPwd);
LBtnPwd.Align := TAlignLayout.None; LBtnPwd.Align := TAlignLayout.Right;
LBtnPwd.SetBounds(76, 0, 70, 54); LBtnPwd.Width := 70;
LBtnPwd.Tag := FFiltered[I]; LBtnPwd.Tag := FFiltered[I];
// CONTENU CONSTRUIT A LA MAIN, sans passer par Text/Detail/Bitmap ni par // CONTENU CONSTRUIT A LA MAIN, sans passer par Text/Detail/Bitmap ni par
// un StyleLookup. Ces gabarits n'existent pas dans tous les styles : // un StyleLookup. Ces gabarits n'existent pas dans tous les styles :