feat(android): folder and 2FA secret can be changed, and the buttons wear icons

The folder had no row of its own - it shared "Folder / tags" with the tags,
so neither could be touched. It is now CHOSEN from the known folders rather
than typed: a typo would file the entry under a folder nothing displays,
here or on the desktop, and it would look lost. Tags stay read-only because
they are an array in the snapshot, and writing one as a string would break
its shape for the desktop.

The 2FA secret gets its own masked row. The code has had a panel for a long
time, but it is computed and renewed every thirty seconds; the secret is what
one sets, changes and removes, and nothing allowed that from the phone.

User, Pass, Copy, Show and Edit become drawings. Four words on a list row left
little room for the entry's name and had to be re-read every time. Every icon
carries an aria-label: replacing a word with a drawing must not take the
information away from someone listening to a screen reader.
This commit is contained in:
r-zakarya
2026-08-21 21:51:42 +01:00
parent c25d68b721
commit 0badc5fcfc
4 changed files with 118 additions and 21 deletions
+87 -12
View File
@@ -157,6 +157,14 @@
/* Rouge sourd : assez pour qu'on ne le confonde pas avec Copy, pas assez
pour attirer l'oeil sur la seule action irreversible de l'ecran. */
.btn.danger { background: rgba(220, 80, 80, .30); }
/* Un bouton d'icone est CARRE et etroit : c'est ce qui le distingue d'un
bouton de texte au premier coup d'oeil, et ce qui laisse la place au nom
de l'entree sur une ligne de liste. La cible reste a 44 px, ce qu'un
doigt attend. */
.btn.ico-btn { padding: 0; width: 44px; min-width: 44px; height: 44px;
display: inline-flex; align-items: center;
justify-content: center; }
.btn.ico-btn svg { width: 20px; height: 20px; }
.empty { text-align: center; color: var(--muted); padding: 48px 16px; }
@@ -539,8 +547,10 @@ function renderRows() {
+ '</div>'
+ '<div class="txt"><b>' + esc(e.name) + '</b>'
+ (e.sub ? '<span>' + esc(e.sub) + '</span>' : '') + '</div>'
+ (e.hasUser ? '<button class="btn" data-copy="user">User</button>' : '')
+ '<button class="btn" data-copy="pwd">' + (e.isNote ? 'Text' : 'Pass')
+ (e.hasUser ? '<button class="btn ico-btn" data-copy="user">'
+ ico('user', 'Copy username') + '</button>' : '')
+ '<button class="btn ico-btn" data-copy="pwd">'
+ (e.isNote ? ico('note', 'Copy text') : ico('key', 'Copy password'))
+ '</button></div>';
}
document.getElementById('rows').innerHTML = shown ? html
@@ -582,6 +592,37 @@ document.getElementById('qx').addEventListener('click', function () {
renderRows();
});
// LES ICONES, dessinees une fois et nommees.
//
// Elles remplacent "User", "Pass", "Copy", "Edit" : quatre mots sur une ligne
// de liste laissaient peu de place au nom de l'entree, et se relisaient a
// chaque fois. Un trait suffit a les distinguer.
//
// aria-label EST OBLIGATOIRE : une icone sans nom n'existe pas pour qui
// n'ecoute que le lecteur d'ecran, et remplacer un mot par un dessin ne doit
// pas retirer l'information a quelqu'un.
var ICONS = {
user: '<path d="M12 12a4 4 0 100-8 4 4 0 000 8z"/>'
+ '<path d="M4.5 20a7.5 7.5 0 0115 0"/>',
key: '<circle cx="8" cy="12" r="3.5"/><path d="M11.5 12H21l-2 2.5"/>'
+ '<path d="M17 12v3"/>',
note: '<path d="M6 3h9l4 4v14H6z"/><path d="M9 12h7M9 16h5"/>',
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/>'
+ '<path d="M15 5H6a2 2 0 00-2 2v9"/>',
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"/>',
eyeOff: '<path d="M4 4l16 16"/>'
+ '<path d="M9.5 5.4A9.8 9.8 0 0112 5c6.5 0 10 6 10 6a17 17 0 01-3.3 3.7"/>'
+ '<path d="M6.3 7.7A17 17 0 002 11s3.5 6 10 6a9.7 9.7 0 003.4-.6"/>'
};
function ico(name, label) {
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"'
+ ' stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"'
+ ' aria-label="' + label + '" role="img">' + ICONS[name] + '</svg>';
}
// --- detail ----------------------------------------------------------------
var detailRows = [];
// Le dernier detail recu, pour redessiner la ligne apres une annulation sans
@@ -603,7 +644,8 @@ function renderDetail(d) {
if (d.totp)
html += '<div class="field totp" id="totp"><div class="cap">2FA code</div>'
+ '<div class="line"><div class="val" id="totpv">------</div>'
+ '<button class="btn" data-copy="totp">Copy</button></div>'
+ '<button class="btn ico-btn" data-copy="totp">'
+ ico('copy', 'Copy code') + '</button></div>'
+ '<div class="bar"><i id="totpb"></i></div></div>';
for (i = 0; i < detailRows.length; i++) {
var r = detailRows[i];
@@ -612,9 +654,12 @@ function renderDetail(d) {
+ '" data-v="' + i + '">'
+ (r.s ? '&#8226;'.repeat(Math.min(r.v.length, 14)) : esc(r.v))
+ '</div>'
+ (r.s ? '<button class="btn" data-show="' + i + '">Show</button>' : '')
+ '<button class="btn" data-copy="row" data-k="' + i + '">Copy</button>'
+ (r.e ? '<button class="btn" data-edit="' + i + '">Edit</button>' : '')
+ (r.s ? '<button class="btn ico-btn" data-show="' + i + '">'
+ ico('eye', 'Show') + '</button>' : '')
+ '<button class="btn ico-btn" data-copy="row" data-k="' + i + '">'
+ ico('copy', 'Copy') + '</button>'
+ (r.e ? '<button class="btn ico-btn" data-edit="' + i + '">'
+ ico('edit', 'Edit') + '</button>' : '')
+ '</div></div>';
}
// SUPPRIMER, tout en bas et en deux temps. Sans retour possible depuis le
@@ -657,7 +702,7 @@ document.getElementById('fields').addEventListener('click', function (ev) {
cell.classList.toggle('mask', !hidden);
cell.textContent = hidden ? detailRows[k].v
: '\u2022'.repeat(Math.min(detailRows[k].v.length, 14));
t.textContent = hidden ? 'Hide' : 'Show';
t.innerHTML = ico(hidden ? 'eyeOff' : 'eye', hidden ? 'Hide' : 'Show');
send('touch');
return;
}
@@ -702,6 +747,40 @@ document.getElementById('fields').addEventListener('click', function (ev) {
}
});
/// LE DOSSIER SE CHOISIT, il ne se tape pas.
///
/// C'est une chaine simple dans l'instantane, mais elle doit correspondre a un
/// dossier existant : une faute de frappe rangerait l'entree dans un dossier
/// que rien n'affiche, ni ici ni sur le poste - l'entree semblerait disparue.
/// La liste des dossiers arrive deja avec le coffre, autant s'en servir.
///
/// 'All' est ajoute d'office : c'est le dossier par defaut du poste, et il ne
/// figure pas toujours dans la liste.
function editorControl(k) {
var val = detailRows[k].v;
var style = ' style="margin:0;flex:1;min-width:0;width:auto"';
if (editField !== 'folder')
return '<input class="field-in" id="editval" autocomplete="off"'
+ ' type="' + (detailRows[k].s ? 'password' : 'text') + '"'
+ ' autocorrect="off" autocapitalize="none" spellcheck="false"'
+ style + '>';
var names = ['All'], i;
for (i = 0; i < folders.length; i++)
if (folders[i] && names.indexOf(folders[i]) < 0) names.push(folders[i]);
// Le dossier courant peut ne plus figurer dans la liste - il a ete renomme
// ailleurs, ou l'entree vient d'un import. Le proposer quand meme evite de
// le remplacer silencieusement en ouvrant simplement l'editeur.
if (val && names.indexOf(val) < 0) names.push(val);
var html = '<select class="field-in" id="editval"' + style + '>';
for (i = 0; i < names.length; i++)
html += '<option value="' + esc(names[i]) + '"'
+ (names[i] === val ? ' selected' : '') + '>' + esc(names[i])
+ '</option>';
return html + '</select>';
}
// EDITEUR EN PLACE, pas window.prompt : une invite JavaScript demande au
// moteur un WebChromeClient que FMX ne fournit pas, et prompt() rend alors
// null sans un mot - le bouton semblerait ne rien faire.
@@ -719,11 +798,7 @@ function openEditor(k) {
editField = detailRows[k].e || '';
if (!editField) return;
var line = row.parentNode;
line.innerHTML =
'<input class="field-in" id="editval" autocomplete="off"'
+ ' type="' + (detailRows[k].s ? 'password' : 'text') + '"'
+ ' autocorrect="off" autocapitalize="none" spellcheck="false"'
+ ' style="margin:0;flex:1;min-width:0;width:auto">'
line.innerHTML = editorControl(k)
// Show n'a de sens que sur un champ masque. Sur un site en clair il
// n'aurait rien a reveler - et l'inverse : il le CACHERAIT.
+ (detailRows[k].s ? '<button class="btn" data-editshow>Show</button>' : '')