From 0619740da16c9ea6e1e2c2c6629df8c0f320349a Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:51:09 +0100 Subject: [PATCH] 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. --- android-client/web/app.html | 76 ++++++++++++++++++++++++++++++------- mainApk.pas | 35 +++++++++++++---- 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/android-client/web/app.html b/android-client/web/app.html index 5d6ef63..6258903 100644 --- a/android-client/web/app.html +++ b/android-client/web/app.html @@ -404,6 +404,7 @@
+
@@ -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 = ''; +/// 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 = ''; + 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); }, diff --git a/mainApk.pas b/mainApk.pas index f513ae2..8acd7ab 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -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')),