add STK menu browser: STK menu pill, menu panel with display text/select item support, OK/Back/Cancel/Timeout buttons, pill locking during menu session

This commit is contained in:
2026-08-08 21:45:56 +03:00
parent 0386c1b224
commit 72ba0c4d87
+151 -1
View File
@@ -851,6 +851,7 @@
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="custom">Custom files</button>
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd">pySim command line</button>
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu">Raw APDU</button>
<button id="stk-menu-btn" onclick="stkMenuOpen()" class="pysim-subtab px-3 py-1 text-xs rounded-full bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button>
</div>
<div id="pysim-sub-files">
@@ -913,8 +914,24 @@
</div>
</div>
<div id="stk-menu-panel" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 max-w-md w-full max-h-[80vh] flex flex-col">
<div id="stk-menu-warning" class="hidden mb-2 text-xs text-amber-600 dark:text-amber-400 text-center bg-amber-50 dark:bg-amber-900/30 rounded px-2 py-1">Menu session blocks all other commands, click Cancel/Exit to leave menu and unblock</div>
<div class="flex justify-between items-center mb-3">
<button id="stk-back-btn" onclick="stkMenuRespond('back')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" disabled>← Back</button>
<span id="stk-menu-panel-title" class="font-bold text-sm"></span>
<button id="stk-cancel-btn" onclick="stkMenuRespond('cancel')" class="px-3 py-1 text-xs rounded-full bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50">Cancel/Exit</button>
</div>
<div id="stk-menu-panel-content" class="flex-1 overflow-auto"></div>
<div id="stk-menu-buttons" class="flex justify-between mt-3 pt-3 border-t border-gray-200 dark:border-slate-700 hidden">
<button onclick="stkMenuRespond('timeout')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600">Timeout</button>
<button onclick="stkMenuRespond('ok')" class="px-4 py-1 text-xs rounded-full bg-blue-600 text-white hover:bg-blue-700">OK</button>
</div>
</div>
</div>
</div>
<script>
// ===== Tab switching =====
function switchTab(name) {
@@ -2543,7 +2560,8 @@ async function pysimConnect() {
pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>';
pysimShowConnected(true);
btn.textContent = 'Connected';
btn.textContent = 'Connected';
stkCheckMenu();
btn.disabled = false;
return;
}
@@ -2720,6 +2738,138 @@ async function pysimSendOta() {
}
}
// ===== STK Menu Browser =====
let stkMenuStack = [];
async function stkCheckMenu() {
try {
const data = await pysimFetch('/api/menu');
const btn = document.getElementById('stk-menu-btn');
if (data && data.title && data.items && data.items.length) {
document.getElementById('stk-menu-title').textContent = data.title;
btn.style.display = '';
} else {
btn.style.display = 'none';
}
} catch (e) {}
}
function stkSetPillsDisabled(disabled) {
document.querySelectorAll('.pysim-subtab').forEach(b => {
if (b.id === 'stk-menu-btn') return;
b.disabled = disabled;
b.classList.toggle('opacity-50', disabled);
});
}
function stkMenuOpen() {
const menu = document.getElementById('stk-menu-btn');
document.getElementById('stk-menu-panel-title').textContent = document.getElementById('stk-menu-title').textContent;
document.getElementById('stk-menu-warning').classList.remove('hidden');
stkSetPillsDisabled(true);
stkMenuStack = [];
document.getElementById('stk-back-btn').disabled = true;
document.getElementById('stk-menu-buttons').classList.remove('hidden');
document.getElementById('stk-menu-panel').classList.remove('hidden');
stkMenuRenderItems();
}
function stkMenuClose() {
document.getElementById('stk-menu-panel').classList.add('hidden');
stkSetPillsDisabled(false);
document.getElementById('stk-menu-warning').classList.add('hidden');
}
function stkMenuRenderItems() {
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '<span class="text-gray-500 text-sm">Loading...</span>';
pysimFetch('/api/menu').then(data => {
if (!data || !data.items) { content.innerHTML = '<span class="text-red-500 text-sm">No menu data</span>'; return; }
let html = '<div class="space-y-1">';
data.items.forEach(item => {
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="stkMenuItemClick(' + item.id + ')"><span class="w-5 text-xs font-bold text-gray-400 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span></div>';
});
html += '</div>';
content.innerHTML = html;
}).catch(() => { content.innerHTML = '<span class="text-red-500 text-sm">Error loading menu</span>'; });
}
async function stkMenuItemClick(itemId) {
stkSetPillsDisabled(true);
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '<span class="text-gray-500 text-sm">Sending...</span>';
try {
const data = await pysimFetch('/api/menu-select', { item_id: itemId });
stkMenuHandleResponse(data);
} catch (e) {
content.innerHTML = '<span class="text-red-500 text-sm">Error: ' + esc(e.message) + '</span>';
stkSetPillsDisabled(false);
}
}
function stkMenuHandleResponse(data) {
const content = document.getElementById('stk-menu-panel-content');
const backBtn = document.getElementById('stk-back-btn');
if (data.type === 'display_text') {
content.innerHTML = '<div class="text-sm whitespace-pre-wrap p-2 bg-gray-50 dark:bg-slate-700 rounded">' + esc(data.text) + '</div>';
backBtn.disabled = stkMenuStack.length === 0;
} else if (data.type === 'select_item') {
stkMenuStack.push(data.items);
content.innerHTML = '';
const items = data.items;
let html = '<div class="space-y-1">';
items.forEach(item => {
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="stkSubItemClick(' + item.id + ')"><span class="w-5 text-xs font-bold text-gray-400 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span></div>';
});
html += '</div>';
content.innerHTML = html;
backBtn.disabled = false;
} else {
content.innerHTML = '<span class="text-gray-500 text-sm">Done</span>';
stkSetPillsDisabled(false);
backBtn.disabled = stkMenuStack.length === 0;
}
}
async function stkSubItemClick(itemId) {
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '<span class="text-gray-500 text-sm">Sending...</span>';
try {
const data = await pysimFetch('/api/menu-respond', { result: 'ok', item_id: itemId });
stkMenuHandleResponse(data);
} catch (e) {
content.innerHTML = '<span class="text-red-500 text-sm">Error: ' + esc(e.message) + '</span>';
}
}
async function stkMenuRespond(result) {
try {
const data = await pysimFetch('/api/menu-respond', { result: result });
if (result === 'cancel') {
stkMenuClose();
return;
}
if (result === 'back') {
if (stkMenuStack.length > 0) {
const prev = stkMenuStack.pop();
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '';
let html = '<div class="space-y-1">';
prev.forEach(item => {
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="stkSubItemClick(' + item.id + ')"><span class="w-5 text-xs font-bold text-gray-400 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span></div>';
});
html += '</div>';
content.innerHTML = html;
document.getElementById('stk-back-btn').disabled = stkMenuStack.length === 0;
}
return;
}
stkMenuHandleResponse(data);
} catch (e) {
document.getElementById('stk-menu-panel-content').innerHTML = '<span class="text-red-500 text-sm">Error: ' + esc(e.message) + '</span>';
}
}
// ===== pysim sub-tabs =====
let pysimCmdHistory = [];
let pysimCmdHistIdx = -1;