feat(android): folder filter, icons, sort, template, updated_at, FLAG_SECURE
Works through the plan's §2 and §3 items that need no hardware and no platform decision. - allowBackup="false". The private directory holds vault.bin; a cloud backup has no business in a password manager's path, even though what would leave is encrypted by a key that stays in the chip. - FLAG_SECURE, Release builds only - in debug it also blocks development screenshots. Complementary to hiding on WillBecomeInactive rather than redundant: that one covers the recents preview, the flag also covers manual captures, screen recording and casting to an external display. - List sorted by name (snapshot order is DB order, arbitrary to the eye), entry icons decoded once per vault rather than on every keystroke, and a folder dropdown whose selection survives a background refresh. No tag filter: text search already matches tags. - template and updated_at read and shown in the detail. updated_at is also step 1 of the write path - nothing is arbitrable without it. Android code is now type-checked without deploying: dccaarm64 reaches the link step and fails only on the NDK's ldandroid.exe. A JNI declaration that does not compile no longer waits for an APK to say so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+118
-8
@@ -24,7 +24,8 @@ interface
|
||||
|
||||
uses
|
||||
System.SysUtils, System.Types, System.UITypes, System.Classes,
|
||||
System.Diagnostics, System.Rtti,
|
||||
System.Diagnostics, System.Rtti, System.NetEncoding,
|
||||
System.Generics.Collections,
|
||||
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform,
|
||||
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
|
||||
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox,
|
||||
@@ -91,8 +92,13 @@ type
|
||||
// --- ecran 2 : liste
|
||||
FLayList: TLayout;
|
||||
FSearch: TEdit;
|
||||
FFolderBox: TComboBox;
|
||||
FList: TListBox;
|
||||
FCount: TLabel;
|
||||
/// Icones decodees une fois par coffre, pas a chaque frappe : la liste est
|
||||
/// reconstruite a chaque caractere tape, et decoder du PNG a ce rythme se
|
||||
/// voit. Cle = indice dans le coffre.
|
||||
FIcons: TObjectDictionary<Integer, TBitmap>;
|
||||
|
||||
// --- ecran 3 : detail
|
||||
// Les lignes sont construites PAR ENTREE : leur nombre depend des champs
|
||||
@@ -139,6 +145,8 @@ type
|
||||
procedure LockVault;
|
||||
|
||||
procedure OnSearchChanged(Sender: TObject);
|
||||
procedure PopulateFolders;
|
||||
procedure RebuildIcons;
|
||||
procedure OnItemTap(const Sender: TCustomListBox; const AItem: TListBoxItem);
|
||||
procedure OnRevealTap(Sender: TObject);
|
||||
procedure OnCopyTap(Sender: TObject);
|
||||
@@ -164,7 +172,8 @@ var
|
||||
implementation
|
||||
|
||||
uses
|
||||
PMA.Crypto, PMA.SyncClient, PMA.Store, PMA.Biometric, PMA.Totp;
|
||||
PMA.Crypto, PMA.SyncClient, PMA.Store, PMA.Biometric, PMA.Totp,
|
||||
PMA.ScreenGuard;
|
||||
|
||||
{$R *.fmx}
|
||||
|
||||
@@ -185,7 +194,9 @@ var
|
||||
begin
|
||||
inherited;
|
||||
Caption := 'Coffre';
|
||||
SecureScreen; // avant tout affichage de contenu
|
||||
FVault := TVault.Create;
|
||||
FIcons := TObjectDictionary<Integer, TBitmap>.Create([doOwnsValues]);
|
||||
|
||||
BuildDetail; // cree en dernier a l'ecran = au-dessus des autres
|
||||
BuildList;
|
||||
@@ -220,6 +231,7 @@ end;
|
||||
|
||||
destructor TForm8.Destroy;
|
||||
begin
|
||||
FIcons.Free;
|
||||
FVault.Free;
|
||||
inherited;
|
||||
end;
|
||||
@@ -299,7 +311,7 @@ end;
|
||||
|
||||
procedure TForm8.BuildList;
|
||||
var
|
||||
LBar: TLayout;
|
||||
LBar, LTop: TLayout;
|
||||
LBtnLock: TButton;
|
||||
begin
|
||||
FLayList := TLayout.Create(Self);
|
||||
@@ -335,10 +347,24 @@ begin
|
||||
FList.ItemHeight := 56;
|
||||
FList.OnItemClick := OnItemTap;
|
||||
|
||||
LTop := TLayout.Create(Self);
|
||||
LTop.Parent := FLayList;
|
||||
LTop.Align := TAlignLayout.Top;
|
||||
LTop.Height := 48;
|
||||
LTop.Margins.Rect := RectF(8, 8, 8, 8);
|
||||
|
||||
// Pas de filtre d'etiquette : la recherche texte couvre deja les etiquettes,
|
||||
// en taper le nom suffit. Les dossiers, eux, ne se devinent pas.
|
||||
FFolderBox := TComboBox.Create(Self);
|
||||
FFolderBox.Parent := LTop;
|
||||
FFolderBox.Align := TAlignLayout.Right;
|
||||
FFolderBox.Width := 140;
|
||||
FFolderBox.Margins.Left := 8;
|
||||
FFolderBox.OnChange := OnSearchChanged;
|
||||
|
||||
FSearch := TEdit.Create(Self);
|
||||
FSearch.Parent := FLayList;
|
||||
FSearch.Align := TAlignLayout.Top;
|
||||
FSearch.Margins.Rect := RectF(8, 8, 8, 8);
|
||||
FSearch.Parent := LTop;
|
||||
FSearch.Align := TAlignLayout.Client;
|
||||
FSearch.TextPrompt := 'Rechercher';
|
||||
FSearch.OnChangeTracking := OnSearchChanged;
|
||||
end;
|
||||
@@ -523,6 +549,12 @@ begin
|
||||
LMeta := LMeta + FCurrent.Tags;
|
||||
end;
|
||||
Add('Dossier / etiquettes', LMeta, False);
|
||||
// Le sous-type n'existe que pour les modeles (carte, cle SSH, serveur...) ;
|
||||
// Add ignore une valeur vide, donc rien a tester ici.
|
||||
Add('Type', FCurrent.Template, False);
|
||||
// Horodatage tel quel : c'est de l'ISO 8601 UTC, lisible, et le reformater
|
||||
// demanderait de decider d'un fuseau alors qu'il n'y a rien a arbitrer.
|
||||
Add('Modifie le', FCurrent.UpdatedAt, False);
|
||||
|
||||
SetLength(FRowValues, Length(FRows));
|
||||
SetLength(FRowShown, Length(FRows));
|
||||
@@ -637,6 +669,7 @@ begin
|
||||
FETag := '';
|
||||
FVault.Clear;
|
||||
FList.Clear;
|
||||
FIcons.Clear;
|
||||
// Les lignes du detail portent des valeurs EN CLAIR (FRows) : les detruire
|
||||
// fait partie du verrouillage, pas de l'affichage.
|
||||
ClearDetailRows;
|
||||
@@ -649,6 +682,8 @@ begin
|
||||
FVault.LoadFromJson(APlainJson);
|
||||
Log(' ' + FVault.LiveCount.ToString + ' entrees, ' +
|
||||
FVault.TrashCount.ToString + ' en corbeille');
|
||||
RebuildIcons;
|
||||
PopulateFolders;
|
||||
// Deja dans l'application : reconstruire la liste SANS y ramener
|
||||
// l'utilisateur. Obligatoire meme s'il est dans le detail - les lignes de
|
||||
// la liste portent des indices vers l'ancien coffre, et un appui ouvrirait
|
||||
@@ -909,16 +944,88 @@ end;
|
||||
Liste et recherche
|
||||
---------------------------------------------------------------------------- }
|
||||
|
||||
/// Data URI -> bitmap, ou nil. Les icones peuvent etre du SVG, que FMX ne
|
||||
/// sait pas charger : l'echec est normal et se solde par les initiales.
|
||||
function BitmapFromDataUri(const AUri: string): TBitmap;
|
||||
var
|
||||
LComma: Integer;
|
||||
LStream: TBytesStream;
|
||||
begin
|
||||
Result := nil;
|
||||
if not AUri.StartsWith('data:') then Exit;
|
||||
LComma := Pos(',', AUri);
|
||||
if LComma = 0 then Exit;
|
||||
try
|
||||
LStream := TBytesStream.Create(TNetEncoding.Base64.DecodeStringToBytes(
|
||||
Copy(AUri, LComma + 1, MaxInt)));
|
||||
try
|
||||
Result := TBitmap.Create;
|
||||
Result.LoadFromStream(LStream);
|
||||
finally
|
||||
LStream.Free;
|
||||
end;
|
||||
except
|
||||
FreeAndNil(Result);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TForm8.RebuildIcons;
|
||||
var
|
||||
I: Integer;
|
||||
LBmp: TBitmap;
|
||||
begin
|
||||
FIcons.Clear;
|
||||
// ponytail: tout decode a l'adoption du coffre, sur le thread principal.
|
||||
// Quelques centaines de vignettes de 16 px ne se sentent pas ; deporter
|
||||
// dans un thread le jour ou l'ouverture marque un temps.
|
||||
for I := 0 to FVault.Count - 1 do
|
||||
begin
|
||||
if FVault.Entry(I).IconB64 = '' then Continue;
|
||||
LBmp := BitmapFromDataUri(FVault.Entry(I).IconB64);
|
||||
if LBmp <> nil then FIcons.Add(I, LBmp);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TForm8.PopulateFolders;
|
||||
var
|
||||
LF, LPrev: string;
|
||||
I: Integer;
|
||||
begin
|
||||
// Le choix courant survit a un rafraichissement de fond : le voir retomber
|
||||
// sur "tous" tout seul, sans avoir rien touche, serait deroutant.
|
||||
LPrev := '';
|
||||
if FFolderBox.ItemIndex > 0 then
|
||||
LPrev := FFolderBox.Items[FFolderBox.ItemIndex];
|
||||
|
||||
FFolderBox.Items.BeginUpdate;
|
||||
try
|
||||
FFolderBox.Items.Clear;
|
||||
FFolderBox.Items.Add('Tous les dossiers');
|
||||
for LF in FVault.Folders do FFolderBox.Items.Add(LF);
|
||||
finally
|
||||
FFolderBox.Items.EndUpdate;
|
||||
end;
|
||||
|
||||
I := FFolderBox.Items.IndexOf(LPrev);
|
||||
if I < 0 then I := 0;
|
||||
FFolderBox.ItemIndex := I;
|
||||
end;
|
||||
|
||||
procedure TForm8.OnSearchChanged(Sender: TObject);
|
||||
var
|
||||
I: Integer;
|
||||
LItem: TListBoxItem;
|
||||
E: TVaultEntry;
|
||||
LFolder: string;
|
||||
LBmp: TBitmap;
|
||||
begin
|
||||
// ponytail: refonte complete de la liste a chaque frappe. Sur un coffre de
|
||||
// quelques centaines d'entrees c'est imperceptible ; passer a un modele
|
||||
// virtuel (TListView) le jour ou ca rame.
|
||||
FFiltered := FVault.Search(FSearch.Text);
|
||||
LFolder := '';
|
||||
if FFolderBox.ItemIndex > 0 then // 0 = tous les dossiers
|
||||
LFolder := FFolderBox.Items[FFolderBox.ItemIndex];
|
||||
FFiltered := FVault.Search(FSearch.Text, LFolder);
|
||||
|
||||
FList.BeginUpdate;
|
||||
try
|
||||
@@ -931,13 +1038,16 @@ begin
|
||||
LItem.Text := E.Display;
|
||||
LItem.ItemData.Detail := E.Username;
|
||||
LItem.StyleLookup := 'listboxitembottomdetail';
|
||||
// Le setter copie le bitmap : le cache garde la propriete du sien.
|
||||
if FIcons.TryGetValue(FFiltered[I], LBmp) then
|
||||
LItem.ItemData.Bitmap := LBmp;
|
||||
LItem.Tag := FFiltered[I]; // indice dans le coffre, pas dans la liste
|
||||
end;
|
||||
finally
|
||||
FList.EndUpdate;
|
||||
end;
|
||||
|
||||
if FSearch.Text.Trim = '' then
|
||||
if (FSearch.Text.Trim = '') and (LFolder = '') then
|
||||
FCount.Text := FVault.LiveCount.ToString + ' entrees'
|
||||
else
|
||||
FCount.Text := Length(FFiltered).ToString + ' / ' +
|
||||
|
||||
Reference in New Issue
Block a user