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
+14 -4
View File
@@ -776,10 +776,20 @@ téléphone qu'on voudra ensuite ouvrir sur le poste demandera un import.
**Sans retour depuis le téléphone**, et c'est délibéré : la corbeille du
poste est un drapeau `deleted` qui ne voyage pas dans la synchronisation, donc
une suppression qui s'y arrêterait ne se propagerait nulle part. D'où le
bouton en bas d'écran, en deux temps, qui se désarme seul au bout de quatre
secondes — un bouton armé indéfiniment se fait toucher par mégarde bien plus
tard.
une suppression qui s'y arrêterait ne se propagerait nulle part.
**Et elle ne va pas non plus dans la corbeille du poste** — vérifié dans le
code serveur : recevoir une pierre tombale déclenche
`DELETE FROM vault_entries`, un effacement dur. Supprimer depuis le téléphone
retire donc la ligne des deux côtés, en contournant la corbeille. **Le seul
filet est la sauvegarde du poste.** Il n'y a rien à afficher comme corbeille
ici : le téléphone n'en reçoit jamais le contenu.
L'action vit dans le **menu de l'entrée** (les trois points, en tête de
détail), et non plus en bouton pleine largeur au bas de chaque écran : rouge
et présent en permanence pour un geste qu'on fait trois fois par an. Deux
gestes délibérés — ouvrir, puis confirmer — et rien qui traîne sous les yeux
le reste du temps.
4. **Réconcilier avec la synchronisation** — fait le 2026-08-21.
Jusqu'ici les deux situations ne se rencontraient pas : un coffre local
+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';