feat(android): follow the desktop's look, and a cross to clear the search

Section captions go uppercase, small and muted, the way the desktop app labels TWO-FACTOR (TOTP) or CUSTOM FIELDS - the eye jumps between captions without confusing them for values. Each field sits in its own bordered box: it is the border that separates values, not the spacing. Buttons take the same thin border instead of a solid grey block.

The 2FA code turns cyan and gains its countdown bar, so a glance says whether there is time to copy it. The seconds leave the text - the bar says it better.

And a cross clears the search, shown only when there is something to clear. A label rather than a button: inside a field, a cross is touched, not commanded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-15 19:19:22 +01:00
parent e128b85658
commit aba873b9cc
2 changed files with 102 additions and 26 deletions
+69 -19
View File
@@ -103,7 +103,7 @@ type
// --- ecran 2 : liste
FLayList: TLayout;
FSearch: TEdit;
FSearchHint: TLabel;
FSearchHint, FSearchClear: TLabel;
/// Choix du dossier : un bouton peint, et un panneau de selection montre
/// a la demande. FFolders[0] est toujours « tous les dossiers ».
FBtnFolder: TRectangle;
@@ -132,6 +132,7 @@ type
// --- TOTP : une ligne vivante, rafraichie chaque seconde
FTotpTimer: TTimer;
FTotpValue: TLabel;
FTotpBar: TRectangle;
FTotpCode: string;
// --- presse-papiers
@@ -175,6 +176,7 @@ type
procedure LockVault;
procedure OnSearchChanged(Sender: TObject);
procedure OnSearchClear(Sender: TObject);
procedure OnFolderTap(Sender: TObject);
procedure OnFolderPick(Sender: TObject);
procedure PopulateFolders;
@@ -454,6 +456,22 @@ begin
FSearchHint.HitTest := False;
FSearchHint.Opacity := 0.45;
FSearchHint.Text := 'Search';
// Croix d'effacement, montree seulement quand il y a quelque chose a
// effacer. Un TLabel et non un bouton : dans un champ, une croix se touche,
// elle ne se dessine pas comme une commande.
FSearchClear := TLabel.Create(FSearch);
FSearchClear.Parent := FSearch;
FSearchClear.Align := TAlignLayout.Right;
FSearchClear.Width := 44;
FSearchClear.Visible := False;
FSearchClear.Text := 'X';
FSearchClear.TextSettings.HorzAlign := TTextAlign.Center;
FSearchClear.TextSettings.VertAlign := TTextAlign.Center;
FSearchClear.StyledSettings := [];
FSearchClear.TextSettings.Font.Size := 17;
FSearchClear.TextSettings.FontColor := COL_MUTED;
FSearchClear.OnClick := OnSearchClear;
end;
/// Intitule discret au-dessus d'une valeur. Meme forme pour toutes les
@@ -469,28 +487,33 @@ function MakeRow(AParent: TFmxObject; const ACaption: string; AOrder: Integer;
out AValue: TLabel): TLayout;
var
LCap: TLabel;
LBox: TRectangle;
begin
// Chaque champ dans son cadre, comme sur le poste Windows : c'est le cadre
// qui separe les valeurs, pas l'espacement.
LBox := Surface(AParent);
LBox.Align := TAlignLayout.Top;
LBox.Index := AOrder;
LBox.Height := 76;
LBox.Margins.Rect := RectF(12, 8, 12, 0);
Result := TLayout.Create(nil); // detruite a la main, voir ClearDetailRows
Result.Parent := AParent;
Result.Index := AOrder;
Result.Align := TAlignLayout.Top;
Result.Height := 62;
Result.Margins.Rect := RectF(12, 10, 12, 0);
Result.Parent := LBox;
Result.Align := TAlignLayout.Client;
Result.Margins.Rect := RectF(12, 8, 12, 8);
Result.TagObject := LBox; // le cadre meurt avec la ligne
LCap := TLabel.Create(Result);
LCap.Parent := Result;
LCap.Align := TAlignLayout.Top;
LCap.Height := 20;
LCap.Height := 18;
LCap.Text := ACaption;
LCap.Opacity := 0.6;
LCap.StyledSettings := LCap.StyledSettings - [TStyledSetting.Size];
LCap.TextSettings.Font.Size := 12;
SectionLabel(LCap);
AValue := TLabel.Create(Result);
AValue.Parent := Result;
AValue.Align := TAlignLayout.Client;
AValue.StyledSettings := AValue.StyledSettings - [TStyledSetting.Size];
AValue.TextSettings.Font.Size := 16;
Tint(AValue, 16);
end;
function AddRowButton(ARow: TLayout; const AText: string; ATag: Integer;
@@ -595,6 +618,7 @@ begin
// Appelable avant la fin du constructeur (ShowList), d'ou la garde.
if FTotpTimer <> nil then FTotpTimer.Enabled := False;
FTotpValue := nil;
FTotpBar := nil;
FTotpCode := '';
// LE CONTENEUR ENTIER EST DETRUIT, pas seulement ses lignes.
//
@@ -604,7 +628,10 @@ begin
// population est fiable, on n'en fait jamais de seconde - un conteneur neuf
// par entree. Ca coute la creation d'un controle, ca supprime une classe
// entiere de bizarreries.
for LRow in FRowCtls do LRow.Free;
// On detruit le CADRE, pas la ligne : la ligne est son enfant et part avec.
for LRow in FRowCtls do
if LRow.TagObject is TRectangle then TRectangle(LRow.TagObject).Free
else LRow.Free;
SetLength(FRowCtls, 0);
FreeAndNil(FDetBody);
SetLength(FRowValues, 0);
@@ -690,8 +717,21 @@ begin
Inc(LOrder);
SetLength(FRowCtls, Length(FRowCtls) + 1);
FRowCtls[High(FRowCtls)] := LRow;
FTotpValue.TextSettings.Font.Size := 26;
FTotpValue.TextSettings.Font.Size := 28;
FTotpValue.TextSettings.FontColor := COL_ACCENT;
AddRowButton(LRow, 'Copy', -1, OnCopyTap);
// Barre de progression sous le code, comme sur le poste : elle dit d'un
// coup d'oeil s'il reste le temps de le recopier.
FTotpBar := TRectangle.Create(LRow);
FTotpBar.Parent := LRow;
FTotpBar.Align := TAlignLayout.None;
FTotpBar.Fill.Color := COL_ACCENT;
FTotpBar.Stroke.Kind := TBrushKind.None;
FTotpBar.XRadius := 1;
FTotpBar.YRadius := 1;
FTotpBar.HitTest := False;
FTotpBar.SetBounds(0, 56, 0, 3); // largeur posee a chaque seconde
OnTotpTick(nil); // pas d'attente d'une seconde a l'ouverture
FTotpTimer.Enabled := True;
end;
@@ -716,8 +756,8 @@ begin
// fait passer pour tronquee alors qu'elle est juste collee.
// La derniere ligne affichee touche sinon le bord du defilement, ce qui la
// fait passer pour tronquee alors qu'elle est juste collee.
if Length(FRows) > 0 then
FRowCtls[High(FRows)].Margins.Bottom := 12;
if (Length(FRows) > 0) and (FRowCtls[High(FRows)].TagObject is TRectangle) then
TRectangle(FRowCtls[High(FRows)].TagObject).Margins.Bottom := 12;
FLayList.Visible := False;
FLayDetail.Visible := True;
@@ -741,10 +781,11 @@ begin
Exit;
end;
// Coupe en deux, comme partout ailleurs : six chiffres d'affilee se
// recopient mal.
// recopient mal. Le temps restant passe dans la barre, plus dans le texte.
FTotpValue.Text := Copy(FTotpCode, 1, Length(FTotpCode) div 2) + ' ' +
Copy(FTotpCode, Length(FTotpCode) div 2 + 1, MaxInt) +
' ' + LLeft.ToString + ' s';
Copy(FTotpCode, Length(FTotpCode) div 2 + 1, MaxInt);
if (FTotpBar <> nil) and (LPeriod > 0) then
FTotpBar.Width := FTotpBar.ParentControl.Width * LLeft / LPeriod;
end;
procedure TForm8.OnBackTap(Sender: TObject);
@@ -1245,6 +1286,14 @@ begin
SetButtonText(FBtnFolder, FFolderNames[FFolderSel]);
end;
procedure TForm8.OnSearchClear(Sender: TObject);
begin
FSearch.Text := '';
// OnChangeTracking ne se declenche pas sur une affectation par code : la
// liste resterait filtree par une requete devenue vide.
OnSearchChanged(nil);
end;
procedure TForm8.OnFolderTap(Sender: TObject);
var
I: Integer;
@@ -1323,6 +1372,7 @@ begin
// virtuel (TListView) le jour ou ca rame.
Touch;
FSearchHint.Visible := FSearch.Text = '';
FSearchClear.Visible := FSearch.Text <> '';
LFolder := '';
if (FFolderSel > 0) and (FFolderSel <= High(FFolderNames)) then
LFolder := FFolderNames[FFolderSel]; // 0 = tous les dossiers