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
+33 -7
View File
@@ -27,6 +27,7 @@ const
COL_BG = TAlphaColor($FF0F172A); // fond general COL_BG = TAlphaColor($FF0F172A); // fond general
COL_SURFACE = TAlphaColor($FF1E293B); // blocs poses dessus COL_SURFACE = TAlphaColor($FF1E293B); // blocs poses dessus
COL_RAISED = TAlphaColor($FF334155); // bouton au repos COL_RAISED = TAlphaColor($FF334155); // bouton au repos
COL_LINE = TAlphaColor($FF334155); // filet des cadres
COL_TEXT = TAlphaColor($FFE2E8F0); COL_TEXT = TAlphaColor($FFE2E8F0);
COL_MUTED = TAlphaColor($FF94A3B8); COL_MUTED = TAlphaColor($FF94A3B8);
COL_ACCENT = TAlphaColor($FF22D3EE); COL_ACCENT = TAlphaColor($FF22D3EE);
@@ -45,9 +46,12 @@ function FlatButton(AParent: TFmxObject; const AText: string;
procedure SetButtonText(AButton: TRectangle; const AText: string); procedure SetButtonText(AButton: TRectangle; const AText: string);
procedure SetButtonEnabled(AButton: TRectangle; AEnabled: Boolean); procedure SetButtonEnabled(AButton: TRectangle; AEnabled: Boolean);
/// Bloc de fond, coins arrondis. /// Bloc de fond, coins arrondis, filet discret.
function Surface(AParent: TFmxObject): TRectangle; function Surface(AParent: TFmxObject): TRectangle;
/// Intitule de section : majuscules, petit, discret.
procedure SectionLabel(ALabel: TLabel);
/// Met un libelle a la palette. AMuted pour les intitules discrets. /// Met un libelle a la palette. AMuted pour les intitules discrets.
procedure Tint(ALabel: TLabel; ASize: Single; AMuted: Boolean = False); procedure Tint(ALabel: TLabel; ASize: Single; AMuted: Boolean = False);
@@ -108,9 +112,23 @@ begin
Result := TRectangle.Create(AParent); Result := TRectangle.Create(AParent);
Result.Parent := AParent; Result.Parent := AParent;
Result.Fill.Color := COL_SURFACE; Result.Fill.Color := COL_SURFACE;
Result.Stroke.Kind := TBrushKind.None; // Filet discret plutot qu'aucune bordure : c'est lui qui detache un bloc du
Result.XRadius := 12; // fond sans avoir a l'eclaircir, comme sur le poste Windows.
Result.YRadius := 12; Result.Stroke.Color := COL_LINE;
Result.Stroke.Thickness := 1;
Result.XRadius := 10;
Result.YRadius := 10;
end;
/// Intitule de section : majuscules, petit, discret. C'est ce qui donne au
/// poste Windows sa lisibilite - l'oeil saute d'un intitule a l'autre sans
/// confondre avec les valeurs.
procedure SectionLabel(ALabel: TLabel);
begin
ALabel.StyledSettings := [];
ALabel.TextSettings.Font.Size := 11;
ALabel.TextSettings.FontColor := COL_MUTED;
ALabel.Text := ALabel.Text.ToUpper;
end; end;
function FlatButton(AParent: TFmxObject; const AText: string; function FlatButton(AParent: TFmxObject; const AText: string;
@@ -121,12 +139,20 @@ var
begin begin
Result := TRectangle.Create(AParent); Result := TRectangle.Create(AParent);
Result.Parent := AParent; Result.Parent := AParent;
Result.Stroke.Kind := TBrushKind.None;
Result.XRadius := 10; Result.XRadius := 10;
Result.YRadius := 10; Result.YRadius := 10;
Result.Cursor := crHandPoint; Result.Cursor := crHandPoint;
if APrimary then Result.Fill.Color := COL_ACCENT if APrimary then
else Result.Fill.Color := COL_RAISED; begin
Result.Fill.Color := COL_ACCENT;
Result.Stroke.Kind := TBrushKind.None;
end
else
begin
Result.Fill.Color := COL_SURFACE;
Result.Stroke.Color := COL_LINE;
Result.Stroke.Thickness := 1;
end;
Result.OnClick := AOnClick; Result.OnClick := AOnClick;
LCaption := TLabel.Create(Result); LCaption := TLabel.Create(Result);
+69 -19
View File
@@ -103,7 +103,7 @@ type
// --- ecran 2 : liste // --- ecran 2 : liste
FLayList: TLayout; FLayList: TLayout;
FSearch: TEdit; FSearch: TEdit;
FSearchHint: TLabel; FSearchHint, FSearchClear: TLabel;
/// Choix du dossier : un bouton peint, et un panneau de selection montre /// Choix du dossier : un bouton peint, et un panneau de selection montre
/// a la demande. FFolders[0] est toujours « tous les dossiers ». /// a la demande. FFolders[0] est toujours « tous les dossiers ».
FBtnFolder: TRectangle; FBtnFolder: TRectangle;
@@ -132,6 +132,7 @@ type
// --- TOTP : une ligne vivante, rafraichie chaque seconde // --- TOTP : une ligne vivante, rafraichie chaque seconde
FTotpTimer: TTimer; FTotpTimer: TTimer;
FTotpValue: TLabel; FTotpValue: TLabel;
FTotpBar: TRectangle;
FTotpCode: string; FTotpCode: string;
// --- presse-papiers // --- presse-papiers
@@ -175,6 +176,7 @@ type
procedure LockVault; procedure LockVault;
procedure OnSearchChanged(Sender: TObject); procedure OnSearchChanged(Sender: TObject);
procedure OnSearchClear(Sender: TObject);
procedure OnFolderTap(Sender: TObject); procedure OnFolderTap(Sender: TObject);
procedure OnFolderPick(Sender: TObject); procedure OnFolderPick(Sender: TObject);
procedure PopulateFolders; procedure PopulateFolders;
@@ -454,6 +456,22 @@ begin
FSearchHint.HitTest := False; FSearchHint.HitTest := False;
FSearchHint.Opacity := 0.45; FSearchHint.Opacity := 0.45;
FSearchHint.Text := 'Search'; 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; end;
/// Intitule discret au-dessus d'une valeur. Meme forme pour toutes les /// 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; out AValue: TLabel): TLayout;
var var
LCap: TLabel; LCap: TLabel;
LBox: TRectangle;
begin 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 := TLayout.Create(nil); // detruite a la main, voir ClearDetailRows
Result.Parent := AParent; Result.Parent := LBox;
Result.Index := AOrder; Result.Align := TAlignLayout.Client;
Result.Align := TAlignLayout.Top; Result.Margins.Rect := RectF(12, 8, 12, 8);
Result.Height := 62; Result.TagObject := LBox; // le cadre meurt avec la ligne
Result.Margins.Rect := RectF(12, 10, 12, 0);
LCap := TLabel.Create(Result); LCap := TLabel.Create(Result);
LCap.Parent := Result; LCap.Parent := Result;
LCap.Align := TAlignLayout.Top; LCap.Align := TAlignLayout.Top;
LCap.Height := 20; LCap.Height := 18;
LCap.Text := ACaption; LCap.Text := ACaption;
LCap.Opacity := 0.6; SectionLabel(LCap);
LCap.StyledSettings := LCap.StyledSettings - [TStyledSetting.Size];
LCap.TextSettings.Font.Size := 12;
AValue := TLabel.Create(Result); AValue := TLabel.Create(Result);
AValue.Parent := Result; AValue.Parent := Result;
AValue.Align := TAlignLayout.Client; AValue.Align := TAlignLayout.Client;
AValue.StyledSettings := AValue.StyledSettings - [TStyledSetting.Size]; Tint(AValue, 16);
AValue.TextSettings.Font.Size := 16;
end; end;
function AddRowButton(ARow: TLayout; const AText: string; ATag: Integer; 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. // Appelable avant la fin du constructeur (ShowList), d'ou la garde.
if FTotpTimer <> nil then FTotpTimer.Enabled := False; if FTotpTimer <> nil then FTotpTimer.Enabled := False;
FTotpValue := nil; FTotpValue := nil;
FTotpBar := nil;
FTotpCode := ''; FTotpCode := '';
// LE CONTENEUR ENTIER EST DETRUIT, pas seulement ses lignes. // 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 // 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 // par entree. Ca coute la creation d'un controle, ca supprime une classe
// entiere de bizarreries. // 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); SetLength(FRowCtls, 0);
FreeAndNil(FDetBody); FreeAndNil(FDetBody);
SetLength(FRowValues, 0); SetLength(FRowValues, 0);
@@ -690,8 +717,21 @@ begin
Inc(LOrder); Inc(LOrder);
SetLength(FRowCtls, Length(FRowCtls) + 1); SetLength(FRowCtls, Length(FRowCtls) + 1);
FRowCtls[High(FRowCtls)] := LRow; FRowCtls[High(FRowCtls)] := LRow;
FTotpValue.TextSettings.Font.Size := 26; FTotpValue.TextSettings.Font.Size := 28;
FTotpValue.TextSettings.FontColor := COL_ACCENT;
AddRowButton(LRow, 'Copy', -1, OnCopyTap); 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 OnTotpTick(nil); // pas d'attente d'une seconde a l'ouverture
FTotpTimer.Enabled := True; FTotpTimer.Enabled := True;
end; end;
@@ -716,8 +756,8 @@ begin
// fait passer pour tronquee alors qu'elle est juste collee. // fait passer pour tronquee alors qu'elle est juste collee.
// La derniere ligne affichee touche sinon le bord du defilement, ce qui la // La derniere ligne affichee touche sinon le bord du defilement, ce qui la
// fait passer pour tronquee alors qu'elle est juste collee. // fait passer pour tronquee alors qu'elle est juste collee.
if Length(FRows) > 0 then if (Length(FRows) > 0) and (FRowCtls[High(FRows)].TagObject is TRectangle) then
FRowCtls[High(FRows)].Margins.Bottom := 12; TRectangle(FRowCtls[High(FRows)].TagObject).Margins.Bottom := 12;
FLayList.Visible := False; FLayList.Visible := False;
FLayDetail.Visible := True; FLayDetail.Visible := True;
@@ -741,10 +781,11 @@ begin
Exit; Exit;
end; end;
// Coupe en deux, comme partout ailleurs : six chiffres d'affilee se // 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) + ' ' + FTotpValue.Text := Copy(FTotpCode, 1, Length(FTotpCode) div 2) + ' ' +
Copy(FTotpCode, Length(FTotpCode) div 2 + 1, MaxInt) + Copy(FTotpCode, Length(FTotpCode) div 2 + 1, MaxInt);
' ' + LLeft.ToString + ' s'; if (FTotpBar <> nil) and (LPeriod > 0) then
FTotpBar.Width := FTotpBar.ParentControl.Width * LLeft / LPeriod;
end; end;
procedure TForm8.OnBackTap(Sender: TObject); procedure TForm8.OnBackTap(Sender: TObject);
@@ -1245,6 +1286,14 @@ begin
SetButtonText(FBtnFolder, FFolderNames[FFolderSel]); SetButtonText(FBtnFolder, FFolderNames[FFolderSel]);
end; 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); procedure TForm8.OnFolderTap(Sender: TObject);
var var
I: Integer; I: Integer;
@@ -1323,6 +1372,7 @@ begin
// virtuel (TListView) le jour ou ca rame. // virtuel (TListView) le jour ou ca rame.
Touch; Touch;
FSearchHint.Visible := FSearch.Text = ''; FSearchHint.Visible := FSearch.Text = '';
FSearchClear.Visible := FSearch.Text <> '';
LFolder := ''; LFolder := '';
if (FFolderSel > 0) and (FFolderSel <= High(FFolderNames)) then if (FFolderSel > 0) and (FFolderSel <= High(FFolderNames)) then
LFolder := FFolderNames[FFolderSel]; // 0 = tous les dossiers LFolder := FFolderNames[FFolderSel]; // 0 = tous les dossiers