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')),