feat(android): delete moves into the entry's menu

It was a full-width red button at the foot of every detail, permanently in
view for a gesture one makes three times a year. It now lives behind the
three dots in the header: open, then confirm - two deliberate gestures, and
nothing lingering in sight the rest of the time.

A single item in a menu is not an oversight. The action has no way back, and
an action with no way back should not sit where a stray tap reaches it. A
cross in the corner would be worse still: that corner means "close"
everywhere else.

The plan records what the server code says about the trash: receiving a
tombstone runs DELETE FROM vault_entries, a hard delete. A deletion from the
phone removes the row on both sides and bypasses the desktop's trash. The
only net is the desktop's backups, and there is no trash to show here - the
phone never receives its contents.
This commit is contained in:
r-zakarya
2026-08-21 23:40:35 +01:00
parent a3a6d8b0f8
commit 00d5b582cc
2 changed files with 87 additions and 30 deletions
+73 -26
View File
@@ -201,6 +201,20 @@
padding: 10px 10px 12px 14px; margin-bottom: 8px;
}
.field .cap { color: var(--muted); font-size: 12px; margin-bottom: 4px; }
/* LE MENU DE L'ENTREE. Il ne contient qu'une action, et c'est voulu : elle
est sans retour, et une action sans retour ne doit pas etre a portee d'un
appui distrait. Deux gestes deliberes - ouvrir, puis confirmer - valent
mieux qu'un bouton rouge en permanence sous les yeux. */
.menu { position: absolute; right: 12px; top: 58px; z-index: 60;
background: var(--bg-top); border-radius: var(--radius-s);
border: 1px solid rgba(255,255,255,.14);
box-shadow: 0 8px 24px rgba(0,0,0,.35);
min-width: 190px; overflow: hidden; }
.menu button { display: block; width: 100%; text-align: left; border: 0;
background: none; color: var(--text); font: inherit;
padding: 14px 16px; }
.menu button:active { background: rgba(255,255,255,.10); }
.menu button.danger { color: #ffb4b4; }
.field .line { display: flex; align-items: center; gap: 8px; }
.field .val {
flex: 1; min-width: 0; word-break: break-all;
@@ -396,8 +410,10 @@
<div class="head">
<button class="btn" id="back">&#8592;</button>
<h1 id="dtitle"></h1>
<button class="btn ico-btn" id="kebab" aria-label="Entry menu"></button>
</div>
<div class="scroll"><div id="fields"></div></div>
<div class="menu" id="menu" hidden></div>
</div>
<div class="toast" id="toast"></div>
@@ -630,6 +646,8 @@ var ICONS = {
edit: '<path d="M4 20h4L20 8l-4-4L4 16z"/>',
eye: '<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/>'
+ '<circle cx="12" cy="12" r="3"/>',
dots: '<circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/>'
+ '<circle cx="12" cy="19" r="1.4"/>',
check: '<path d="M4 12.5l5.5 5.5L20 7"/>',
close: '<path d="M6 6l12 12M18 6L6 18"/>',
// Un arc, pas un cercle : c'est l'ouverture qui rend la rotation visible.
@@ -684,15 +702,8 @@ function renderDetail(d) {
+ ico('edit', 'Edit') + '</button>' : '')
+ '</div></div>';
}
// SUPPRIMER, tout en bas et en deux temps. Sans retour possible depuis le
// telephone - il n'y a pas de corbeille ici - donc ni en tete d'ecran ni a
// portee d'un appui distrait. Le premier appui ne fait qu'armer ; c'est la
// confirmation, faute de pouvoir en demander une au moteur (une invite
// JavaScript exige un WebChromeClient que FMX ne fournit pas).
html += '<div class="field"><div class="line">'
+ '<button class="btn danger" id="del" style="flex:1">Delete entry'
+ '</button></div></div>';
document.getElementById('fields').innerHTML = html;
closeMenu();
document.body.classList.add('on-detail');
view = 'detail';
send('state?view=detail');
@@ -703,24 +714,6 @@ document.getElementById('fields').addEventListener('click', function (ev) {
// l'intention. Tout ce qui suit lit et ecrit sur le bouton.
var t = ev.target.closest('button') || ev.target;
var k = t.getAttribute('data-show');
if (t.id === 'del') {
if (t.getAttribute('data-armed') === null) {
t.setAttribute('data-armed', '1');
t.textContent = 'Really delete - tap again';
// Desarme tout seul : un bouton reste arme indefiniment se fait toucher
// par megarde bien plus tard, et la suppression est sans retour.
setTimeout(function () {
if (!t.parentNode) return;
t.removeAttribute('data-armed');
t.textContent = 'Delete entry';
}, 4000);
return;
}
t.disabled = true;
t.textContent = 'Deleting...';
send('delentry');
return;
}
if (k !== null) {
var cell = document.querySelector('[data-v="' + k + '"]');
var hidden = cell.classList.contains('mask');
@@ -1041,7 +1034,61 @@ document.getElementById('fields').addEventListener('click', function (ev) {
});
document.getElementById('new').addEventListener('click', openNewEntry);
// LE MENU DE L'ENTREE, et la suppression qui y vit.
//
// Elle etait un bouton pleine largeur au bas de chaque detail : rouge, present
// en permanence, pour une action qu'on fait trois fois par an. Ici il faut
// l'ouvrir, puis confirmer - deux gestes deliberes, et rien qui traine sous
// les yeux le reste du temps.
//
// SANS RETOUR, toujours : il n'y a pas de corbeille sur le telephone, et la
// 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 = '';
}
function openMenu() {
var m = document.getElementById('menu');
m.innerHTML = '<button class="danger" data-del>Delete entry</button>';
m.hidden = false;
}
document.getElementById('kebab').innerHTML = ico('dots', 'Entry menu');
document.getElementById('kebab').addEventListener('click', function (ev) {
ev.stopPropagation();
if (document.getElementById('menu').hidden) openMenu(); else closeMenu();
});
document.getElementById('menu').addEventListener('click', function (ev) {
var b = ev.target.closest('button');
if (!b) return;
if (b.getAttribute('data-del') !== null) {
// Le premier appui n'arme que la confirmation. Ouvrir le menu etait deja
// delibere ; ceci ferme la porte a l'appui qui derape dans la liste.
b.removeAttribute('data-del');
b.setAttribute('data-delconfirm', '1');
b.textContent = 'Confirm delete';
return;
}
if (b.getAttribute('data-delconfirm') === null) return;
b.disabled = true;
b.textContent = 'Deleting...';
send('delentry');
});
// 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;
closeMenu();
});
function goList() {
closeMenu();
document.body.classList.remove('on-detail');
document.body.classList.add('on-list');
view = 'list';