diff --git a/index.html b/index.html index c56e1d0..dbf0b8a 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@
-

OTAMan SIM OTA with a Human Face v1.0.0

+

OTAMan SIM OTA with a Human Face v1.1.0

github @@ -845,6 +845,7 @@
+ + @@ -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 ? ' (' + node.fid.toUpperCase() + ')' : ''; const aidStr = node.aid ? ' [' + node.aid + ']' : ''; - let html = '
'; + let html = '
'; html += pad; - if (!exists) { + if (verified && !exists) { html += '✗ '; } if (isDir) { - if (exists) { + if ((verified && exists) || !verified) { const toggle = node.children && node.expanded ? '▼' : '▶'; - html += '' + toggle + ' '; - html += '' + esc(node.name) + fidStr + aidStr + ''; + html += '' + toggle + ' '; + html += '' + esc(node.name) + fidStr + aidStr + ''; } else { html += esc(node.name) + fidStr + aidStr + '
'; return html; } } else { - html += '' + esc(node.name) + fidStr + aidStr + ''; + html += '' + esc(node.name) + fidStr + aidStr + ''; } html += '
'; 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 = 'No custom files defined.'; return; } + let html = ''; + for (let i = 0; i < pysimCustomFiles.length; i++) { + const c = pysimCustomFiles[i]; + html += '
'; + html += '' + esc(c.path) + ' ' + esc(c.name) + ''; + html += ''; + html += '
'; + } + el.innerHTML = html; +} + +// Init custom files +pysimCustomLoad(); + // Init sub-tab pills document.querySelectorAll('.pysim-subtab').forEach(btn => { btn.addEventListener('click', () => pysimSwitchSubtab(btn.dataset.pysimSub)); diff --git a/style.css b/style.css index ee1dadd..2487bb2 100644 --- a/style.css +++ b/style.css @@ -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));