feat(android): a selection bar in the list, and Duplicate everywhere

The floating menu was wrong for a list. It closed on the next tap, so opening
an entry afterwards took two gestures and felt like a button had got stuck.

Android's contextual bar instead: a long press selects, the top bar takes the
place of search, says how many rows are held, and offers duplicate and
delete. It goes away when nothing is held. One does not search while choosing
what to delete, and two rows of controls would fight over the same thumb.

Selection is multiple, because the cost is in the bar, not in the work: the
patch closure loops, and ten deletions become ONE publish. The container goes
up whole on every write - deleting ten entries one by one would pay that trip
ten times, and give nine chances to fail halfway.

A tap during selection picks rather than opens, which is also the way out:
unpicking the last row closes the bar. The hardware back cancels the
selection before locking the vault - locking because someone wanted to cancel
a choice is the opposite of what the gesture means.

Both menus stay, with distinct jobs: the bar acts on rows chosen in the list,
the detail's three dots act on the entry in front of you. Duplicate copies
everything, attachments included, as the desktop does - a copy that silently
lost the attached document would be a surprise discovered too late.
This commit is contained in:
r-zakarya
2026-08-22 00:10:38 +01:00
parent 0619740da1
commit ebd4752ec9
3 changed files with 261 additions and 61 deletions
+60
View File
@@ -65,6 +65,14 @@ function AddEntry(const APlainJson, ATitle, ASite, AUsername, APassword,
/// une suppression qui s'y arreterait ne se propagerait donc nulle part.
function DeleteEntry(const APlainJson, AUuid: string): string;
/// Duplique une entree : meme contenu, uuid neuf, nom suffixe.
///
/// COPIE INTEGRALE, pieces jointes comprises. Le poste fait de meme, et une
/// copie qui perdrait le document attache serait une surprise le jour ou l'on
/// s'en apercevrait - c'est-a-dire trop tard. Le prix est un instantane plus
/// lourd a publier ; il se voit tout de suite, lui.
function DuplicateEntry(const APlainJson, AUuid: string): string;
/// Fond l'instantane LOCAL dans l'instantane DISTANT et rend le resultat.
///
/// LE DISTANT SERT DE BASE, jamais le local. Il porte ce que le telephone ne
@@ -258,6 +266,58 @@ begin
end;
end;
function DuplicateEntry(const APlainJson, AUuid: string): string;
var
LRoot, LEntry, LCopy: TJSONObject;
LEntries: TJSONArray;
I: Integer;
LNow, LTitle: string;
begin
if AUuid = '' then
raise EPMAPatch.Create('Entree sans uuid : rien a dupliquer');
LRoot := TJSONObject.ParseJSONValue(APlainJson) as TJSONObject;
if LRoot = nil then
raise EPMAPatch.Create('Instantane illisible');
try
if not LRoot.TryGetValue<TJSONArray>('entries', LEntries) then
raise EPMAPatch.Create('Instantane sans tableau "entries"');
LCopy := nil;
for I := 0 to LEntries.Count - 1 do
begin
if not (LEntries.Items[I] is TJSONObject) then Continue;
LEntry := LEntries.Items[I] as TJSONObject;
if LEntry.GetValue<string>('uuid', '') <> AUuid then Continue;
// Reparser plutot que recopier champ par champ : c'est le seul moyen qui
// n'oublie rien de ce que le telephone ne comprend pas.
LCopy := CloneJson(LEntry);
Break;
end;
if LCopy = nil then
raise EPMAPatch.CreateFmt('Entree %s introuvable', [AUuid]);
LNow := NowUtcIso;
LCopy.RemovePair('uuid').Free;
LCopy.AddPair('uuid', LowerCase(Copy(GUIDToString(TGUID.NewGuid), 2, 36)));
LCopy.RemovePair('created_at').Free;
LCopy.AddPair('created_at', LNow);
LCopy.RemovePair('updated_at').Free;
LCopy.AddPair('updated_at', LNow);
// Le nom change, sinon les deux lignes sont indiscernables dans la liste.
// Retirer PUIS ajouter, comme partout ici : AddPair seul laisserait deux
// paires 'title', et le lecteur prendrait la premiere - donc l'ancienne.
LTitle := Trim(LCopy.GetValue<string>('title', '') + ' (copy)');
LCopy.RemovePair('title').Free;
LCopy.AddPair('title', LTitle);
LEntries.AddElement(LCopy);
Result := LRoot.ToJSON;
finally
LRoot.Free;
end;
end;
function DeleteEntry(const APlainJson, AUuid: string): string;
var
LRoot, LEntry, LStone: TJSONObject;