feat(apk): afficher/masquer les secrets, scanner le QR 2FA, et toast() reparé

- Mot de passe et secret 2FA naissent masques dans la creation et l'edition,
  avec un oeil pour les reveler.
- Un bouton QR dans le champ 2FA ouvre la camera du scan des reglages ; le lien
  otpauth:// lu part a la page, qui n'en garde que le secret. Le journal ne note
  que le fait.
- toast() designait l'element <div id="toast"> (chaque id est une variable
  globale) : sept appels de la page levaient "toast is not a function" et leur
  message ne s'affichait jamais. Une vraie fonction globale les repare tous.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-28 11:20:31 +01:00
parent 22c418f933
commit 3acf78b1ca
2 changed files with 85 additions and 9 deletions
+64 -6
View File
@@ -1043,6 +1043,8 @@ var ICONS = {
+ '<circle cx="9" cy="9" r="1.2"/><circle cx="15" cy="15" r="1.2"/>'
+ '<circle cx="15" cy="9" r="1.2"/><circle cx="9" cy="15" r="1.2"/>',
close: '<path d="M6 6l12 12M18 6L6 18"/>',
qr: '<rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/>'
+ '<rect x="4" y="14" width="6" height="6" rx="1"/><path d="M14 14h2v2h-2zM20 14v.01M14 20h.01M17 20h3v-3"/>',
// Un arc, pas un cercle : c'est l'ouverture qui rend la rotation visible.
loader: '<path d="M12 3a9 9 0 019 9"/>',
eyeOff: '<path d="M4 4l16 16"/>'
@@ -1141,6 +1143,18 @@ document.getElementById('fields').addEventListener('click', function (ev) {
send('touch');
return;
}
var eye = t.getAttribute('data-eye');
if (eye) {
var ef = document.getElementById(eye);
if (!ef) return;
var shown = ef.type === 'text';
ef.type = shown ? 'password' : 'text';
t.innerHTML = ico(shown ? 'eye' : 'eyeOff', shown ? 'Show' : 'Hide');
send('touch');
return;
}
var sc = t.getAttribute('data-scan');
if (sc) { totpTarget = sc; send('scanqr?for=totp'); return; }
var ia = t.getAttribute('data-iconact');
if (ia !== null) {
// L'ICONE SE COMMET COMME LE RESTE, donc Delphi rouvre l'entree et la vue
@@ -1379,6 +1393,29 @@ function genPassword(n) {
}
return out;
}
/// AFFICHER / MASQUER. Le champ nait masque : on ouvre un formulaire dans un
/// lieu public autant qu'on le remplit, et le mot de passe n'a pas a
/// s'afficher tant qu'on ne le demande pas. Un appui le revele pour relire ce
/// qu'on tape.
function eyeButton(target) {
return '<button class="btn ico-btn" data-eye="' + target + '">'
+ ico('eye', 'Show') + '</button>';
}
/// SCANNER LE QR QU'AFFICHE LE SITE a l'activation de la 2FA. La camera est
/// celle du scan des reglages ; seul ce qu'elle cherche change.
function scanButton(target) {
return '<button class="btn ico-btn" data-scan="' + target + '">'
+ ico('qr', 'Scan the 2FA QR code') + '</button>';
}
/// UN VRAI toast() GLOBAL. Sans lui, le nom designait l'element <div
/// id="toast"> - le navigateur expose chaque id comme variable globale - et
/// chaque appel toast('...') de cette page levait "toast is not a function" :
/// le message ne s'affichait pas, et le reste du gestionnaire ne s'executait
/// pas davantage. Une declaration de fonction l'emporte sur l'id.
function toast(text) { PM.toast(text); }
function genButton(target) {
return '<button class="btn ico-btn" data-gen="' + target + '">'
+ ico('dice', 'Generate') + '</button>';
@@ -1389,9 +1426,9 @@ function newBodyControl(kind) {
return '<textarea class="field-in" id="nPwd" rows="6" enterkeyhint="enter"'
+ ' autocorrect="off" spellcheck="false" style="' + FIELD_STYLE
+ ';height:auto;padding:10px 14px;resize:vertical"></textarea>';
return '<input class="field-in" id="nPwd" type="text" autocomplete="off"'
return '<input class="field-in" id="nPwd" type="password" autocomplete="off"'
+ ' autocorrect="off" autocapitalize="none" spellcheck="false"'
+ ' style="' + FIELD_STYLE + '">' + genButton('nPwd');
+ ' style="' + FIELD_STYLE + '">' + eyeButton('nPwd') + genButton('nPwd');
}
function openNewEntry() {
@@ -1423,7 +1460,11 @@ function openNewEntry() {
// deja : on pouvait poser un second facteur sur une entree existante, pas
// sur une entree qu'on venait de creer - il fallait enregistrer puis
// rouvrir. Meme intitule que l'editeur pour qu'on reconnaisse le champ.
+ newField('nTotp', '2FA secret', 'nTotpRow')
+ '<div class="field" id="nTotpRow"><div class="cap">2FA secret</div>'
+ '<div class="line"><input class="field-in" id="nTotp" type="password"'
+ ' autocomplete="off" autocorrect="off" autocapitalize="characters"'
+ ' spellcheck="false" style="' + FIELD_STYLE + '">'
+ eyeButton('nTotp') + scanButton('nTotp') + '</div></div>'
+ '</div>'
+ '<div class="line" style="display:flex;margin-top:12px">'
// flex:1 et non .wide : .wide ne regle que le rembourrage, et la largeur
@@ -1530,6 +1571,8 @@ function openMenu() {
/// Ce que le formulaire porte a cet instant, par nom de champ. Sert a le
/// rouvrir tel quel apres une action d'icone, qui passe par un enregistrement.
var editAgain = null;
/// Le champ 2FA qui attend le resultat du scan - creation ou edition.
var totpTarget = '';
/// Le formulaire occupe l'ecran du detail : 'view' reste donc 'detail', et
/// c'est ce drapeau qui dit qu'on y modifie - la fleche et la touche retour
/// s'en servent pour refermer le formulaire avant de quitter l'entree.
@@ -1564,11 +1607,15 @@ function openEditEntry() {
? '<textarea class="field-in" data-ef="' + esc(r.e) + '" rows="5"'
+ ' enterkeyhint="enter" autocorrect="off" spellcheck="false"'
+ ' style="' + FIELD_STYLE + ';height:auto;resize:vertical"></textarea>'
: '<input class="field-in"' + (r.e === 'password' ? ' id="editpwd"' : '')
+ ' data-ef="' + esc(r.e) + '" type="text"'
: '<input class="field-in"'
+ (r.e === 'password' ? ' id="editpwd"'
: r.e === 'totp_secret' ? ' id="edittotp"' : '')
+ ' data-ef="' + esc(r.e) + '" type="'
+ (r.e === 'password' || r.e === 'totp_secret' ? 'password' : 'text') + '"'
+ ' autocomplete="off" autocorrect="off" autocapitalize="none"'
+ ' spellcheck="false" style="' + FIELD_STYLE + '">')
+ (r.e === 'password' ? genButton('editpwd') : '')
+ (r.e === 'password' ? eyeButton('editpwd') + genButton('editpwd') : '')
+ (r.e === 'totp_secret' ? eyeButton('edittotp') + scanButton('edittotp') : '')
+ '</div></div>';
}
// L'ICONE SE CHANGE OU SE CHANGENT LES CHAMPS. Elle vivait dans le menu de
@@ -2250,6 +2297,17 @@ var PM = {
if (view === 'detail') goList();
},
cancelEdit: function () { renderDetail(lastDetail); },
/// Le QR d'un site porte otpauth://totp/Label?secret=XXXX&issuer=... - on
/// n'en garde que le secret, comme le champ l'attend. Un QR sans secret
/// (un autre lien) ne touche a rien.
totpFromQr: function (uri) {
var m = /[?&]secret=([^&#]+)/i.exec(uri || '');
var el = document.getElementById(totpTarget);
if (!m || !el) { PM.toast('Not a 2FA QR code'); return; }
el.value = decodeURIComponent(m[1]).replace(/\s+/g, '').toUpperCase();
PM.toast('2FA secret read from the QR code');
},
/// La touche retour annule la selection avant de verrouiller le coffre.
/// Sans cela, un geste d'annulation fermait le coffre - le contraire de ce
/// qu'il veut dire.