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:
@@ -27,6 +27,7 @@ const
|
||||
COL_BG = TAlphaColor($FF0F172A); // fond general
|
||||
COL_SURFACE = TAlphaColor($FF1E293B); // blocs poses dessus
|
||||
COL_RAISED = TAlphaColor($FF334155); // bouton au repos
|
||||
COL_LINE = TAlphaColor($FF334155); // filet des cadres
|
||||
COL_TEXT = TAlphaColor($FFE2E8F0);
|
||||
COL_MUTED = TAlphaColor($FF94A3B8);
|
||||
COL_ACCENT = TAlphaColor($FF22D3EE);
|
||||
@@ -45,9 +46,12 @@ function FlatButton(AParent: TFmxObject; const AText: string;
|
||||
procedure SetButtonText(AButton: TRectangle; const AText: string);
|
||||
procedure SetButtonEnabled(AButton: TRectangle; AEnabled: Boolean);
|
||||
|
||||
/// Bloc de fond, coins arrondis.
|
||||
/// Bloc de fond, coins arrondis, filet discret.
|
||||
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.
|
||||
procedure Tint(ALabel: TLabel; ASize: Single; AMuted: Boolean = False);
|
||||
|
||||
@@ -108,9 +112,23 @@ begin
|
||||
Result := TRectangle.Create(AParent);
|
||||
Result.Parent := AParent;
|
||||
Result.Fill.Color := COL_SURFACE;
|
||||
Result.Stroke.Kind := TBrushKind.None;
|
||||
Result.XRadius := 12;
|
||||
Result.YRadius := 12;
|
||||
// Filet discret plutot qu'aucune bordure : c'est lui qui detache un bloc du
|
||||
// fond sans avoir a l'eclaircir, comme sur le poste Windows.
|
||||
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;
|
||||
|
||||
function FlatButton(AParent: TFmxObject; const AText: string;
|
||||
@@ -121,12 +139,20 @@ var
|
||||
begin
|
||||
Result := TRectangle.Create(AParent);
|
||||
Result.Parent := AParent;
|
||||
Result.Stroke.Kind := TBrushKind.None;
|
||||
Result.XRadius := 10;
|
||||
Result.YRadius := 10;
|
||||
Result.Cursor := crHandPoint;
|
||||
if APrimary then Result.Fill.Color := COL_ACCENT
|
||||
else Result.Fill.Color := COL_RAISED;
|
||||
if APrimary then
|
||||
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;
|
||||
|
||||
LCaption := TLabel.Create(Result);
|
||||
|
||||
+69
-19
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user