feat(android): entries can be created on the phone

A vault you cannot add to is not a vault, so an account created here was
useless without this. A plus button next to the padlock, a form, and a
kind - login or note, which the vault distinguishes and whose body is the
password field either way.

One save path now, two transports, and the rule that decides between them
is: if a local container exists it is the authority. A vault born here has
no repository; a vault pulled from one has no local container. The case
where both exist - a local vault that later gains a repository - needs a
merge, and that is section 7 step 4.

The patch arrives as a function rather than an already-patched snapshot:
the remote path must modify WHAT IT HAS JUST PULLED, or it would overwrite
whatever another device published in between. That constraint is why the
two transports could not simply share a finished payload.

Two traps closed while passing: the form left the typed password sitting
in the document after the save, and a refusal left its Save button dead
with the text behind it.

Verified on the device that the button and the form appear and read
correctly. Saving was not exercised - the vault on this phone syncs to a
real repository, and a test entry would have been published to it with no
way to delete it from here yet.
This commit is contained in:
r-zakarya
2026-08-21 14:22:08 +01:00
parent f32cde81fc
commit d65fe909b4
3 changed files with 295 additions and 71 deletions
+55
View File
@@ -40,6 +40,17 @@ function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): strin
/// naisse pas dans un dialecte a part.
function NewEmptySnapshot(const AUsername: string): string;
/// Ajoute une entree a l'instantane et rend le JSON complet.
///
/// LA MEME FORME QUE CE QU'ECRIT LE POSTE, cles vides comprises : une entree
/// nee ici sera relue la-bas, et une cle absente ne se comporte pas toujours
/// comme une cle vide. `folder` vaut 'All' parce que c'est le dossier par
/// defaut du poste, pas parce que c'est joli.
///
/// Un uuid neuf, et lui seul : pas de tombstone, on ne supprime pas encore.
function AddEntry(const APlainJson, ATitle, ASite, AUsername, APassword,
AKind: string): string;
/// Horodatage UTC au format que tout le reste emploie (toISOString cote JS).
/// MELANGER heure locale et UTC a deja decale l'arbitrage de la fusion une
/// fois : il n'y a qu'une source d'heure ici, et elle est en UTC.
@@ -75,6 +86,50 @@ begin
end;
end;
function AddEntry(const APlainJson, ATitle, ASite, AUsername, APassword,
AKind: string): string;
var
LRoot, LEntry: TJSONObject;
LEntries: TJSONArray;
LNow: string;
begin
LRoot := TJSONObject.ParseJSONValue(APlainJson) as TJSONObject;
if LRoot = nil then
raise EPMAPatch.Create('Snapshot is not a JSON object');
try
LEntries := LRoot.GetValue('entries') as TJSONArray;
if LEntries = nil then
raise EPMAPatch.Create('Snapshot has no entries array');
LNow := NowUtcIso;
LEntry := TJSONObject.Create;
// Minuscules et sans accolades : la forme que tout le reste emploie.
LEntry.AddPair('uuid', LowerCase(Copy(GUIDToString(TGUID.NewGuid), 2, 36)));
LEntry.AddPair('title', ATitle);
LEntry.AddPair('site', ASite);
LEntry.AddPair('username', AUsername);
// Le corps d'une note EST son mot de passe cote stockage - il n'y a pas de
// colonne separee, ni ici ni sur le poste.
LEntry.AddPair('password', APassword);
LEntry.AddPair('folder', 'All');
LEntry.AddPair('tags', TJSONArray.Create);
LEntry.AddPair('totp_secret', '');
LEntry.AddPair('kind', AKind);
LEntry.AddPair('template', '');
LEntry.AddPair('custom_fields', TJSONArray.Create);
LEntry.AddPair('attachments', TJSONArray.Create);
LEntry.AddPair('icon_b64', '');
LEntry.AddPair('icon_custom', TJSONNumber.Create(0));
LEntry.AddPair('created_at', LNow);
LEntry.AddPair('updated_at', LNow);
LEntries.AddElement(LEntry);
Result := LRoot.ToJSON;
finally
LRoot.Free;
end;
end;
function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): string;
var
LRoot, LEntry: TJSONObject;