refactor(android): the unlock screen moves to HTML, Skia and the style go

The caret in an Android TEdit does not land where you touch, which makes a long bearer token impossible to correct - the reason the last FMX screen had to go. A field in the engine inherits the system's text handling (caret, selection, handles, paste) for free.

With no FMX control left, two things became dead weight and are gone: the vendored Dark.style, which had nothing left to style, and Skia - 25 MB carried solely so FMX labels would shape Arabic. The engine does its own shaping; checked on the device, an Arabic filename still reads correctly. The APK drops from 22.0 to 12.7 MB.

Delphi stays the owner of the server settings; the page only edits them and hands them back when a field loses focus, one navigation per changed field rather than one per keystroke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-17 10:40:06 +01:00
parent d1d5d29879
commit 77809b09c8
8 changed files with 244 additions and 5914 deletions
+119 -4
View File
@@ -58,7 +58,12 @@
detail - dont les blocs sont eux-memes du blanc translucide. */
background: linear-gradient(var(--bg-top), var(--bg-bot));
}
#detail { transform: translateX(100%); }
/* Trois vues sur un rail. Le deverrouillage est a gauche, la liste au
milieu, le detail a droite ; deux classes sur le corps disent ou on est. */
#list, #detail { transform: translateX(100%); }
body.on-list #unlock { transform: translateX(-100%); }
body.on-list #list { transform: translateX(0); }
body.on-detail #unlock { transform: translateX(-100%); }
body.on-detail #list { transform: translateX(-22%); }
body.on-detail #detail { transform: translateX(0); }
@@ -163,6 +168,26 @@
.bar i { display: block; height: 100%; background: var(--text);
transition: width 1s linear; }
/* --- deverrouillage ----------------------------------------------------- */
.status { color: var(--muted); margin: 22px 4px 14px; min-height: 20px; }
.stack { display: flex; flex-direction: column; gap: 8px; }
.pair { display: flex; gap: 8px; }
.pair .btn { flex: 1; }
.field-in {
width: 100%; height: 46px; padding: 0 14px; margin-top: 8px;
border: 0; border-radius: var(--radius-s);
background: rgba(255,255,255,.10); color: var(--text);
font: inherit; outline: none;
}
.field-in::placeholder { color: var(--muted); }
#log {
margin: 10px 0 0; padding: 10px 12px; max-height: 34vh; overflow: auto;
background: rgba(0,0,0,.20); border-radius: var(--radius-s);
font: 11px/1.45 monospace; white-space: pre-wrap; word-break: break-all;
-webkit-user-select: text; user-select: text;
}
[hidden] { display: none !important; }
.toast {
position: fixed; left: 12px; right: 12px; bottom: 14px;
background: rgba(10,20,40,.92); color: var(--text);
@@ -173,6 +198,31 @@
.toast.on { opacity: 1; }
</style>
<div class="screen" id="unlock">
<div class="scroll">
<div class="status" id="status"></div>
<div class="stack">
<button class="btn wide main" id="btnOpen">&#10140;&nbsp; Open vault</button>
<button class="btn wide" id="btnSync">&#8635;&nbsp; Sync</button>
<div class="pair">
<button class="btn" id="btnCfg">&#9881;&nbsp; Server settings</button>
<button class="btn" id="btnLog">&#8801;&nbsp; Log</button>
</div>
</div>
<div id="cfg" hidden>
<input class="field-in" id="url" placeholder="endpoint URL"
autocomplete="off" autocorrect="off" autocapitalize="none"
spellcheck="false" inputmode="url">
<input class="field-in" id="tok" placeholder="bearer token"
autocomplete="off" autocorrect="off" autocapitalize="none"
spellcheck="false">
<input class="field-in" id="pwd" placeholder="sync password"
type="password" autocomplete="off">
</div>
<pre id="log" hidden></pre>
</div>
</div>
<div class="screen" id="list">
<div class="top">
<div class="search">
@@ -240,7 +290,41 @@ function drain() {
}, 60);
}
var entries = [], folders = [], folder = '', query = '', view = 'list';
var entries = [], folders = [], folder = '', query = '', view = 'unlock';
// --- deverrouillage --------------------------------------------------------
var elUrl = document.getElementById('url'),
elTok = document.getElementById('tok'),
elPwd = document.getElementById('pwd');
// Les trois valeurs remontent A LA PERTE DU FOCUS, pas a chaque touche : c'est
// une navigation par champ modifie au lieu d'une par caractere. Delphi les
// garde, la page n'est qu'un editeur.
function pushConfig() {
send('cfg?u=' + encodeURIComponent(elUrl.value.trim())
+ '&t=' + encodeURIComponent(elTok.value.trim())
+ '&p=' + encodeURIComponent(elPwd.value));
}
[elUrl, elTok, elPwd].forEach(function (el) {
el.addEventListener('change', pushConfig);
el.addEventListener('blur', pushConfig);
});
document.getElementById('btnOpen').addEventListener('click', function () {
send('openvault');
});
document.getElementById('btnSync').addEventListener('click', function () {
pushConfig(); // ce qui est tape mais pas encore quitte compte aussi
send('sync');
});
document.getElementById('btnCfg').addEventListener('click', function () {
var c = document.getElementById('cfg');
c.hidden = !c.hidden;
});
document.getElementById('btnLog').addEventListener('click', function () {
var l = document.getElementById('log');
l.hidden = !l.hidden;
});
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
@@ -376,6 +460,7 @@ document.getElementById('fields').addEventListener('click', function (ev) {
function goList() {
document.body.classList.remove('on-detail');
document.body.classList.add('on-list');
view = 'list';
// Les valeurs en clair partent avec la vue, pas seulement de l'ecran.
detailRows = [];
@@ -393,10 +478,40 @@ var PM = {
folder = '';
q.value = ''; query = '';
document.body.classList.remove('searching', 'on-detail');
document.body.classList.add('on-list');
view = 'list';
renderFolders();
renderRows();
},
showUnlock: function () {
document.body.classList.remove('on-list', 'on-detail', 'searching');
view = 'unlock';
},
setStatus: function (text) {
document.getElementById('status').textContent = text;
},
/// Etat des deux actions : ouvrir n'a de sens qu'avec un coffre local
/// lisible, et synchroniser s'appelle autrement quand il n'y en a pas.
setActions: function (canOpen, syncLabel, busy) {
var o = document.getElementById('btnOpen'), s = document.getElementById('btnSync');
o.hidden = !canOpen;
s.innerHTML = '&#8635;&nbsp; ' + syncLabel;
o.disabled = busy; s.disabled = busy;
o.style.opacity = busy ? .4 : 1;
s.style.opacity = busy ? .4 : 1;
},
setConfig: function (url, tok, pwd, show) {
elUrl.value = url; elTok.value = tok; elPwd.value = pwd;
// Replier des champs vides revient a cacher la seule chose a faire.
document.getElementById('cfg').hidden = !show;
},
log: function (line) {
var l = document.getElementById('log');
l.textContent += line + '\n';
l.scrollTop = l.scrollHeight;
},
clearLog: function () { document.getElementById('log').textContent = ''; },
setDetail: function (json) { renderDetail(JSON.parse(json)); },
setTotp: function (code, left, period) {
var v = document.getElementById('totpv');
@@ -424,8 +539,8 @@ var PM = {
document.getElementById('fields').innerHTML = '';
document.getElementById('folders').innerHTML = '';
q.value = '';
document.body.classList.remove('on-detail', 'searching');
view = 'list';
document.body.classList.remove('on-list', 'on-detail', 'searching');
view = 'unlock';
}
};
window.PM = PM;