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:
+151
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user