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:
+73
-26
@@ -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">←</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';
|
||||
|
||||
Reference in New Issue
Block a user