fix(android): delete from the list, and stop reopening what was deleted

Two things, and the second was me misreading the question: the affordance was
asked for in the LIST, not in the opened entry.

A long press on a row opens the same menu. Not a cross per row - an
irreversible action a thumb's width away on a list one scrolls - and not a
third button per row, when those were just cleared away. A long press is
invisible until used, it is Android's convention for acting on a list item,
and it is where multi-select would go if it ever comes.

And the deleted entry reopened itself. editDone hands control back after a
refusal, but it redrew the LAST detail - after a delete the view has already
returned to the list, so the entry one had just erased came back on screen.
It now hands back to what is on screen, not to what was.

Delphi deletes by rank when the list asks, by the open entry otherwise -
exactly like open and copy. The page never sends a uuid: it has one, but
letting it name a row Delphi never displayed is a door with no reason to be
open.
This commit is contained in:
r-zakarya
2026-08-21 23:51:09 +01:00
parent 00d5b582cc
commit 0619740da1
2 changed files with 91 additions and 20 deletions
+63 -13
View File
@@ -404,6 +404,7 @@
</div>
<div class="folders" id="folders"></div>
<div class="scroll"><div id="rows"></div></div>
<div class="menu" id="menuList" hidden></div>
</div>
<div class="screen" id="detail">
@@ -596,9 +597,12 @@ document.getElementById('rows').addEventListener('click', function (ev) {
// de copier. Le doigt tombant tantot sur le glyphe tantot sur la marge, cela
// ne ratait qu'une fois sur deux - la pire des pannes a diagnostiquer.
var btn = ev.target.closest('[data-copy]');
if (btn) send('copy?i=' + row.getAttribute('data-i')
+ '&f=' + btn.getAttribute('data-copy'));
else send('open?i=' + row.getAttribute('data-i'));
if (btn) { send('copy?i=' + row.getAttribute('data-i')
+ '&f=' + btn.getAttribute('data-copy')); return; }
// Le menu vient de s'ouvrir sur cette ligne : le clic qui suit l'appui long
// ouvrirait l'entree DERRIERE le menu.
if (!document.getElementById('menuList').hidden) return;
send('open?i=' + row.getAttribute('data-i'));
});
document.getElementById('folders').addEventListener('click', function (ev) {
@@ -1045,14 +1049,23 @@ document.getElementById('new').addEventListener('click', openNewEntry);
// pierre tombale fait disparaitre l'entree du poste aussi, sans passer par la
// sienne. Le seul filet est la sauvegarde du poste.
function closeMenu() {
var m = document.getElementById('menu');
m.hidden = true;
m.innerHTML = '';
['menu', 'menuList'].forEach(function (id) {
var m = document.getElementById(id);
if (!m) return;
m.hidden = true;
m.innerHTML = '';
});
}
function openMenu() {
var m = document.getElementById('menu');
m.innerHTML = '<button class="danger" data-del>Delete entry</button>';
/// AMenu vit dans l'ecran courant : le detail a le sien, la liste le sien.
/// ATop, quand il est donne, ancre le menu a la hauteur de la ligne touchee -
/// un menu qui apparait loin du doigt ne se rattache a rien.
function openMenu(AIndex, ATop) {
var m = document.getElementById(view === 'list' ? 'menuList' : 'menu');
m.innerHTML = '<button class="danger" data-del'
+ (AIndex === undefined ? '' : ' data-i="' + AIndex + '"')
+ '>Delete entry</button>';
if (ATop !== undefined) m.style.top = ATop + 'px';
m.hidden = false;
}
@@ -1062,7 +1075,7 @@ document.getElementById('kebab').addEventListener('click', function (ev) {
if (document.getElementById('menu').hidden) openMenu(); else closeMenu();
});
document.getElementById('menu').addEventListener('click', function (ev) {
function onMenuClick(ev) {
var b = ev.target.closest('button');
if (!b) return;
if (b.getAttribute('data-del') !== null) {
@@ -1076,14 +1089,47 @@ document.getElementById('menu').addEventListener('click', function (ev) {
if (b.getAttribute('data-delconfirm') === null) return;
b.disabled = true;
b.textContent = 'Deleting...';
send('delentry');
});
// L'index designe l'entree quand on supprime depuis la liste. Absent, c'est
// l'entree ouverte - Delphi tient deja laquelle.
var i = b.getAttribute('data-i');
send('delentry' + (i === null ? '' : '?i=' + i));
}
document.getElementById('menu').addEventListener('click', onMenuClick);
document.getElementById('menuList').addEventListener('click', onMenuClick);
// APPUI LONG POUR SUPPRIMER DEPUIS LA LISTE.
//
// Ni une croix par ligne - une action sans retour a un pouce d'ecart sur une
// liste qu'on fait defiler - ni un troisieme bouton par ligne, alors qu'on
// vient justement de les degager. L'appui long ne se voit pas tant qu'on ne
// s'en sert pas, c'est la convention d'Android pour agir sur un element de
// liste, et c'est la que se rangera la selection multiple si elle vient.
(function () {
var timer = null, moved = false;
var rows = document.getElementById('rows');
rows.addEventListener('touchstart', function (ev) {
var row = ev.target.closest('.row');
// Pas sur les boutons de copie : ils ont deja leur geste.
if (!row || ev.target.closest('[data-copy]')) return;
moved = false;
timer = setTimeout(function () {
if (moved) return;
var r = row.getBoundingClientRect();
openMenu(row.getAttribute('data-i'), Math.round(r.bottom) - 40);
send('touch');
}, 600);
});
rows.addEventListener('touchmove', function () { moved = true; clearTimeout(timer); });
['touchend', 'touchcancel'].forEach(function (e) {
rows.addEventListener(e, function () { clearTimeout(timer); });
});
})();
// Un appui n'importe ou ailleurs referme le menu - y compris sur le contenu,
// sinon il resterait ouvert pendant qu'on lit l'entree.
document.addEventListener('click', function (ev) {
if (document.getElementById('menu').hidden) return;
if (ev.target.closest('#menu, #kebab')) return;
if (ev.target.closest('#menu, #menuList, #kebab')) return;
closeMenu();
});
@@ -1128,6 +1174,10 @@ var PM = {
// derriere - a retaper sans savoir pourquoi.
var save = document.getElementById('nSave');
if (save) { save.disabled = false; return; }
// RENDRE LA MAIN A CE QUI EST A L'ECRAN, pas a ce qui y etait. Apres une
// suppression la vue est deja revenue a la liste, et redessiner le dernier
// detail rouvrait l'entree qu'on venait d'effacer - elle semblait revenue.
if (view !== 'detail') return;
if (lastDetail) renderDetail(lastDetail);
},
+28 -7
View File
@@ -276,8 +276,9 @@ type
/// comme 'setfield' - meme regle, meme journalisation muette.
procedure CreateEntry(const ATitle, ASite, AUser, APwd, AKind,
AFolder: string);
/// Supprime l'entree ouverte, pierre tombale comprise. Sans retour.
procedure DeleteCurrentEntry;
/// Supprime une entree, pierre tombale comprise. Sans retour.
/// AVaultIndex negatif = l'entree ouverte.
procedure DeleteEntryAt(AVaultIndex: Integer);
/// Fond le coffre ne ici dans le depot, puis passe l'autorite au depot.
procedure ReconcileWithRemote;
procedure ReopenEntryByUuid(const AUuid: string);
@@ -1173,17 +1174,37 @@ begin
end).Start;
end;
procedure TForm8.DeleteCurrentEntry;
procedure TForm8.DeleteEntryAt(AVaultIndex: Integer);
var
LUuid, LTitle: string;
E: TVaultEntry;
begin
if FCurrent.Uuid = '' then
// 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
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');
Exit;
end;
LUuid := FCurrent.Uuid;
LTitle := FCurrent.Display;
// Pas de rang a rouvrir : l'entree n'existera plus. AdoptVault ramene la
// liste, ce qui est exactement ou l'on veut se retrouver.
CommitChange('Deleted: ' + LTitle, 'entry deleted from phone', '',
@@ -1357,7 +1378,7 @@ begin
TNetEncoding.URL.Decode(Param('d')))
else if LAction = 'delentry' then
DeleteCurrentEntry
DeleteEntryAt(StrToIntDef(Param('i'), -1))
else if LAction = 'setfield' then
SaveEntryField(TNetEncoding.URL.Decode(Param('f')),