bump to v1.1.0 — parameter hints, custom files, i18n, version compatibility
This commit is contained in:
+136
-11
@@ -16,7 +16,7 @@
|
||||
<div class="max-w-4xl mx-auto px-6 py-4">
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.0.0</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.1.0</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
|
||||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
@@ -845,6 +845,7 @@
|
||||
<div id="pysim-body" class="hidden">
|
||||
<div class="flex gap-1 mb-2">
|
||||
<button class="pysim-subtab px-3 py-1 text-xs rounded-full bg-blue-600 text-white" data-pysim-sub="files">File manager</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="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>
|
||||
</div>
|
||||
@@ -870,12 +871,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-custom" class="hidden">
|
||||
<div class="mb-2 text-xs font-mono text-gray-600 dark:text-slate-400">Add known file paths and aliases to the file tree.</div>
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<input id="pysim-cf-path" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="3F00/6F46" onkeydown="if(event.key==='Enter')pysimCustomAdd()">
|
||||
<input id="pysim-cf-name" class="font-mono w-48 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="EF.SPN" onkeydown="if(event.key==='Enter')pysimCustomAdd()">
|
||||
<button onclick="pysimCustomAdd()" class="px-3 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap">Add</button>
|
||||
</div>
|
||||
<div id="pysim-cf-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-cmd" class="hidden">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<input id="pysim-cmd" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="select MF" autocomplete="off" list="pysim-cmdlist" onkeydown="pysimCmdKeydown(event)">
|
||||
<input id="pysim-cmd" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 dark:bg-slate-800" placeholder="select MF" autocomplete="off" list="pysim-cmdlist" onkeydown="pysimCmdKeydown(event)" oninput="pysimCmdHint()">
|
||||
<datalist id="pysim-cmdlist"></datalist>
|
||||
<button onclick="pysimExec()" class="px-4 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700">Execute</button>
|
||||
</div>
|
||||
<div id="pysim-cmd-hint" class="hidden mb-2 text-xs font-mono text-gray-500 dark:text-slate-400 border border-gray-200 dark:border-slate-700 rounded p-2 bg-gray-50 dark:bg-slate-800 whitespace-pre-wrap"></div>
|
||||
<textarea id="pysim-cmd-output" rows="10" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||||
</div>
|
||||
|
||||
@@ -2640,10 +2652,11 @@ function pysimSwitchSubtab(name) {
|
||||
btn.classList.toggle('text-gray-700', !isActive);
|
||||
btn.classList.toggle('dark:text-slate-300', !isActive);
|
||||
});
|
||||
['cmd', 'apdu', 'files'].forEach(s => {
|
||||
['cmd', 'apdu', 'files', 'custom'].forEach(s => {
|
||||
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
|
||||
});
|
||||
if (name === 'files') pysimFsRefresh();
|
||||
if (name === 'custom') pysimCustomRender();
|
||||
}
|
||||
|
||||
function pysimCmdKeydown(e) {
|
||||
@@ -2654,6 +2667,7 @@ function pysimCmdKeydown(e) {
|
||||
pysimCmdHistIdx = pysimCmdHistory.length;
|
||||
}
|
||||
pysimExec();
|
||||
document.getElementById('pysim-cmd-hint').classList.add('hidden');
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (pysimCmdHistIdx > 0) {
|
||||
@@ -2672,6 +2686,33 @@ function pysimCmdKeydown(e) {
|
||||
}
|
||||
}
|
||||
|
||||
let pysimCmdHintTimeout = null;
|
||||
|
||||
async function pysimCmdHint() {
|
||||
clearTimeout(pysimCmdHintTimeout);
|
||||
pysimCmdHintTimeout = setTimeout(async () => {
|
||||
const input = document.getElementById('pysim-cmd');
|
||||
const text = input.value.trim();
|
||||
const cmd = text.split(/\s+/)[0];
|
||||
if (!cmd || text.includes(' ')) {
|
||||
document.getElementById('pysim-cmd-hint').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
const hintEl = document.getElementById('pysim-cmd-hint');
|
||||
try {
|
||||
const data = await pysimFetch('/api/help', { cmd });
|
||||
if (data.usage) {
|
||||
hintEl.textContent = data.usage;
|
||||
hintEl.classList.remove('hidden');
|
||||
} else {
|
||||
hintEl.classList.add('hidden');
|
||||
}
|
||||
} catch (e) {
|
||||
hintEl.classList.add('hidden');
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
async function pysimLoadCommands() {
|
||||
const list = document.getElementById('pysim-cmdlist');
|
||||
try {
|
||||
@@ -2694,6 +2735,31 @@ function getParentSel(node) {
|
||||
return p.name;
|
||||
}
|
||||
|
||||
function pysimCustomInject(node) {
|
||||
if (!node || !node.fid) return;
|
||||
const pfid = node.fid.toLowerCase();
|
||||
for (const c of pysimCustomFiles) {
|
||||
if (c.parentFid.toLowerCase() !== pfid) continue;
|
||||
const existing = (node.children || []).find(ch => ch.fid === c.fid);
|
||||
if (existing) {
|
||||
existing.name = c.name;
|
||||
existing.custom = true;
|
||||
} else {
|
||||
node.children = node.children || [];
|
||||
node.children.push({
|
||||
name: c.name,
|
||||
fid: c.fid,
|
||||
isDir: c.name.startsWith('DF.') || c.name.startsWith('ADF.'),
|
||||
children: null,
|
||||
expanded: false,
|
||||
exists: null,
|
||||
custom: true,
|
||||
parent: node,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function pysimFsRefresh() {
|
||||
const detailEl = document.getElementById('pysim-fs-detail');
|
||||
detailEl.classList.add('hidden');
|
||||
@@ -2718,6 +2784,7 @@ async function pysimFsRefresh() {
|
||||
parent: pysimFsTreeRoot,
|
||||
}));
|
||||
pysimFsTreeRoot.exists = true;
|
||||
pysimCustomInject(pysimFsTreeRoot);
|
||||
pysimFsRenderTree();
|
||||
}
|
||||
|
||||
@@ -2746,6 +2813,7 @@ async function pysimFsLoadChildren(node) {
|
||||
parent: node,
|
||||
}));
|
||||
node.exists = true;
|
||||
pysimCustomInject(node);
|
||||
} catch (e) {
|
||||
node.exists = false;
|
||||
node.children = [];
|
||||
@@ -2764,26 +2832,28 @@ function pysimFsRenderNode(node, depth) {
|
||||
const pad = ' '.repeat(depth * 4);
|
||||
const isDir = node.isDir;
|
||||
const exists = node.exists !== false;
|
||||
const color = exists ? (isDir ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-slate-300') : 'text-red-400 dark:text-red-400';
|
||||
const verified = node.exists !== null;
|
||||
const color = verified ? (exists ? (isDir ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-slate-300') : 'text-red-400 dark:text-red-400') : 'text-gray-500 dark:text-slate-400';
|
||||
const italic = node.custom ? ' italic' : '';
|
||||
const fidStr = node.fid ? ' <span class="text-gray-400 dark:text-slate-500">(' + node.fid.toUpperCase() + ')</span>' : '';
|
||||
const aidStr = node.aid ? ' <span class="text-gray-400 dark:text-slate-500">[' + node.aid + ']</span>' : '';
|
||||
let html = '<div class="' + color + '">';
|
||||
let html = '<div class="' + color + italic + '">';
|
||||
html += pad;
|
||||
if (!exists) {
|
||||
if (verified && !exists) {
|
||||
html += '✗ ';
|
||||
}
|
||||
if (isDir) {
|
||||
if (exists) {
|
||||
if ((verified && exists) || !verified) {
|
||||
const toggle = node.children && node.expanded ? '▼' : '▶';
|
||||
html += '<span class="cursor-pointer select-none" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + toggle + '</span> ';
|
||||
html += '<span class="cursor-pointer hover:underline" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + esc(node.name) + fidStr + aidStr + '</span>';
|
||||
html += '<span class="cursor-pointer select-none' + italic + '" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + toggle + '</span> ';
|
||||
html += '<span class="cursor-pointer hover:underline' + italic + '" onclick="pysimFsToggleDir(\'' + esc(node.name) + '\')">' + esc(node.name) + fidStr + aidStr + '</span>';
|
||||
} else {
|
||||
html += esc(node.name) + fidStr + aidStr + '</div>';
|
||||
return html;
|
||||
}
|
||||
} else {
|
||||
html += '<span class="' + (exists ? 'cursor-pointer hover:underline ml-4' : 'ml-4') + '"' +
|
||||
(exists ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : '') + '>' + esc(node.name) + fidStr + aidStr + '</span>';
|
||||
html += '<span class="cursor-pointer hover:underline ml-4' + italic + '"' +
|
||||
((verified && exists) ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : (!verified ? ' onclick="pysimFsClickFile(\'' + esc(node.name) + '\')"' : '')) + '>' + esc(node.name) + fidStr + aidStr + '</span>';
|
||||
}
|
||||
html += '</div>';
|
||||
if (isDir && node.children && node.expanded) {
|
||||
@@ -2915,6 +2985,61 @@ function pysimFsSetMode(mode) {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== custom files =====
|
||||
let pysimCustomFiles = [];
|
||||
|
||||
function pysimCustomLoad() {
|
||||
try {
|
||||
const d = localStorage.getItem('pysim_custom_files');
|
||||
pysimCustomFiles = d ? JSON.parse(d) : [];
|
||||
} catch (e) { pysimCustomFiles = []; }
|
||||
}
|
||||
|
||||
function pysimCustomSave() {
|
||||
localStorage.setItem('pysim_custom_files', JSON.stringify(pysimCustomFiles));
|
||||
}
|
||||
|
||||
function pysimCustomAdd() {
|
||||
const pathEl = document.getElementById('pysim-cf-path');
|
||||
const nameEl = document.getElementById('pysim-cf-name');
|
||||
const path = pathEl.value.trim().toUpperCase();
|
||||
const name = nameEl.value.trim().toUpperCase();
|
||||
if (!path || !name) return;
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
if (parts.length < 2) { alert('Path must have at least 2 parts, e.g. 3F00/6F46'); return; }
|
||||
const fid = parts[parts.length - 1];
|
||||
const parentFid = parts[parts.length - 2];
|
||||
if (pysimCustomFiles.some(c => c.path === path)) { alert('Path already exists'); return; }
|
||||
pysimCustomFiles.push({ path, name, fid, parentFid });
|
||||
pysimCustomSave();
|
||||
pysimCustomRender();
|
||||
pathEl.value = '';
|
||||
nameEl.value = '';
|
||||
}
|
||||
|
||||
function pysimCustomRemove(i) {
|
||||
pysimCustomFiles.splice(i, 1);
|
||||
pysimCustomSave();
|
||||
pysimCustomRender();
|
||||
}
|
||||
|
||||
function pysimCustomRender() {
|
||||
const el = document.getElementById('pysim-cf-list');
|
||||
if (!pysimCustomFiles.length) { el.innerHTML = '<span class="text-gray-400">No custom files defined.</span>'; return; }
|
||||
let html = '';
|
||||
for (let i = 0; i < pysimCustomFiles.length; i++) {
|
||||
const c = pysimCustomFiles[i];
|
||||
html += '<div class="flex items-center gap-2 py-1 border-b border-gray-200 dark:border-slate-700">';
|
||||
html += '<span class="text-gray-500">' + esc(c.path) + '</span> <span class="text-blue-600 dark:text-blue-400">→</span> <b>' + esc(c.name) + '</b>';
|
||||
html += '<span onclick="pysimCustomRemove(' + i + ')" class="ml-auto cursor-pointer text-red-400 hover:text-red-600">✕</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// Init custom files
|
||||
pysimCustomLoad();
|
||||
|
||||
// Init sub-tab pills
|
||||
document.querySelectorAll('.pysim-subtab').forEach(btn => {
|
||||
btn.addEventListener('click', () => pysimSwitchSubtab(btn.dataset.pysimSub));
|
||||
|
||||
@@ -599,6 +599,10 @@ video {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
@@ -663,6 +667,10 @@ video {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-48 {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.w-6 {
|
||||
width: 1.5rem;
|
||||
}
|
||||
@@ -769,6 +777,10 @@ video {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -802,6 +814,11 @@ video {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
||||
@@ -962,6 +979,10 @@ video {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.leading-relaxed {
|
||||
line-height: 1.625;
|
||||
}
|
||||
@@ -1073,6 +1094,11 @@ video {
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-red-600:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-red-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
|
||||
@@ -1099,6 +1125,11 @@ video {
|
||||
border-color: rgb(71 85 105 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.dark\:border-slate-700:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(51 65 85 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
|
||||
|
||||
Reference in New Issue
Block a user