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:
+84
-34
@@ -276,9 +276,12 @@ type
|
||||
/// comme 'setfield' - meme regle, meme journalisation muette.
|
||||
procedure CreateEntry(const ATitle, ASite, AUser, APwd, AKind,
|
||||
AFolder: string);
|
||||
/// Supprime une entree, pierre tombale comprise. Sans retour.
|
||||
/// AVaultIndex negatif = l'entree ouverte.
|
||||
procedure DeleteEntryAt(AVaultIndex: Integer);
|
||||
function UuidsFromIndexes(const AList: string): TArray<string>;
|
||||
/// Supprime des entrees, pierres tombales comprises. Sans retour.
|
||||
/// Liste vide = l'entree ouverte.
|
||||
procedure DeleteEntriesAt(const AList: string);
|
||||
/// Duplique des entrees. Liste vide = l'entree ouverte.
|
||||
procedure DuplicateEntriesAt(const AList: string);
|
||||
/// Fond le coffre ne ici dans le depot, puis passe l'autorite au depot.
|
||||
procedure ReconcileWithRemote;
|
||||
procedure ReopenEntryByUuid(const AUuid: string);
|
||||
@@ -504,7 +507,15 @@ begin
|
||||
end;
|
||||
// C'est la PAGE qui sait ce qu'elle affiche - Delphi ne le devine pas. Elle
|
||||
// le declare a chaque changement de vue (cmd://state).
|
||||
if FWebView = 'detail' then
|
||||
// UNE SELECTION EN COURS S'ANNULE D'ABORD. Verrouiller le coffre parce qu'on
|
||||
// a voulu annuler un choix serait le contraire de ce que le geste veut dire.
|
||||
if FWebView = 'select' then
|
||||
begin
|
||||
WebCall('PM.clearSelection()');
|
||||
FWebView := 'list';
|
||||
Key := 0;
|
||||
end
|
||||
else if FWebView = 'detail' then
|
||||
begin
|
||||
WebCall('PM.back()');
|
||||
FWebView := 'list';
|
||||
@@ -1174,43 +1185,79 @@ begin
|
||||
end).Start;
|
||||
end;
|
||||
|
||||
procedure TForm8.DeleteEntryAt(AVaultIndex: Integer);
|
||||
/// LES RANGS ENVOYES PAR LA PAGE -> LES UUID CORRESPONDANTS.
|
||||
///
|
||||
/// Une liste vide veut dire "l'entree ouverte" : c'est le menu du detail, qui
|
||||
/// n'a pas de rang a donner. La page n'envoie jamais d'uuid - elle en a un,
|
||||
/// mais le faire voyager reviendrait a lui laisser designer une ligne que
|
||||
/// Delphi n'a pas affichee.
|
||||
function TForm8.UuidsFromIndexes(const AList: string): TArray<string>;
|
||||
var
|
||||
LUuid, LTitle: string;
|
||||
E: TVaultEntry;
|
||||
LPart: string;
|
||||
LIdx: Integer;
|
||||
begin
|
||||
// UN RANG, OU L'ENTREE OUVERTE. Depuis la liste il n'y a pas d'entree
|
||||
// ouverte : le rang designe laquelle, exactement comme pour 'open' et
|
||||
// 'copy'. La page n'envoie jamais d'uuid - elle en a un, mais le faire
|
||||
// voyager reviendrait a lui laisser designer une ligne que Delphi n'a pas
|
||||
// affichee.
|
||||
if AVaultIndex >= 0 then
|
||||
Result := nil;
|
||||
if Trim(AList) = '' then
|
||||
begin
|
||||
if AVaultIndex >= FVault.Count then
|
||||
begin
|
||||
RefuseChange('That entry is no longer there');
|
||||
Exit;
|
||||
end;
|
||||
E := FVault.Entry(AVaultIndex);
|
||||
LUuid := E.Uuid;
|
||||
LTitle := E.Display;
|
||||
end
|
||||
else
|
||||
begin
|
||||
LUuid := FCurrent.Uuid;
|
||||
LTitle := FCurrent.Display;
|
||||
end;
|
||||
if LUuid = '' then
|
||||
begin
|
||||
RefuseChange('This entry has no uuid - cannot be deleted');
|
||||
if FCurrent.Uuid <> '' then Result := [FCurrent.Uuid];
|
||||
Exit;
|
||||
end;
|
||||
// Pas de rang a rouvrir : l'entree n'existera plus. AdoptVault ramene la
|
||||
for LPart in AList.Split([',']) do
|
||||
begin
|
||||
LIdx := StrToIntDef(Trim(LPart), -1);
|
||||
if (LIdx < 0) or (LIdx >= FVault.Count) then Continue;
|
||||
if FVault.Entry(LIdx).Uuid = '' then Continue;
|
||||
Result := Result + [FVault.Entry(LIdx).Uuid];
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TForm8.DeleteEntriesAt(const AList: string);
|
||||
var
|
||||
LUuids: TArray<string>;
|
||||
begin
|
||||
LUuids := UuidsFromIndexes(AList);
|
||||
if Length(LUuids) = 0 then
|
||||
begin
|
||||
RefuseChange('Nothing to delete');
|
||||
Exit;
|
||||
end;
|
||||
// UNE SEULE PUBLICATION POUR TOUTES. Le conteneur monte en entier a chaque
|
||||
// ecriture : supprimer dix entrees une par une couterait dix fois ce trajet,
|
||||
// et neuf occasions de le voir echouer a mi-chemin.
|
||||
//
|
||||
// Pas de rang a rouvrir : les entrees n'existeront plus. AdoptVault ramene la
|
||||
// liste, ce qui est exactement ou l'on veut se retrouver.
|
||||
CommitChange('Deleted: ' + LTitle, 'entry deleted from phone', '',
|
||||
CommitChange(Format('Deleted %d', [Length(LUuids)]),
|
||||
'entries deleted from phone', '',
|
||||
function (APlain: string): string
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := DeleteEntry(APlain, LUuid);
|
||||
Result := APlain;
|
||||
for I := 0 to High(LUuids) do
|
||||
Result := DeleteEntry(Result, LUuids[I]);
|
||||
end);
|
||||
end;
|
||||
|
||||
procedure TForm8.DuplicateEntriesAt(const AList: string);
|
||||
var
|
||||
LUuids: TArray<string>;
|
||||
begin
|
||||
LUuids := UuidsFromIndexes(AList);
|
||||
if Length(LUuids) = 0 then
|
||||
begin
|
||||
RefuseChange('Nothing to duplicate');
|
||||
Exit;
|
||||
end;
|
||||
CommitChange(Format('Duplicated %d', [Length(LUuids)]),
|
||||
'entries duplicated from phone', '',
|
||||
function (APlain: string): string
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := APlain;
|
||||
for I := 0 to High(LUuids) do
|
||||
Result := DuplicateEntry(Result, LUuids[I]);
|
||||
end);
|
||||
end;
|
||||
|
||||
@@ -1378,7 +1425,10 @@ begin
|
||||
TNetEncoding.URL.Decode(Param('d')))
|
||||
|
||||
else if LAction = 'delentry' then
|
||||
DeleteEntryAt(StrToIntDef(Param('i'), -1))
|
||||
DeleteEntriesAt(TNetEncoding.URL.Decode(Param('i')))
|
||||
|
||||
else if LAction = 'dupentry' then
|
||||
DuplicateEntriesAt(TNetEncoding.URL.Decode(Param('i')))
|
||||
|
||||
else if LAction = 'setfield' then
|
||||
SaveEntryField(TNetEncoding.URL.Decode(Param('f')),
|
||||
|
||||
Reference in New Issue
Block a user