connected state redesign: sim/nosim svg, spin animation, dark:invert, pysimSetConnected()

This commit is contained in:
2026-08-07 01:04:00 +03:00
parent 2a91eb3c68
commit bfb0ceaaa3
3 changed files with 89 additions and 38 deletions
+30 -18
View File
@@ -827,13 +827,17 @@
<p>To connect, install <a href="https://github.com/anttro/pysim-otaman-server" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-otaman-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/pysim-otaman-server.git</code> and start it. Refer to README.md for details. Click <b>Connect</b> once pysim-otaman-server is ready.</p> <p>To connect, install <a href="https://github.com/anttro/pysim-otaman-server" target="_blank" class="text-blue-600 dark:text-blue-400 underline">pysim-otaman-server</a>: <code class="font-mono bg-gray-100 dark:bg-slate-700 px-1 rounded">git clone https://github.com/anttro/pysim-otaman-server.git</code> and start it. Refer to README.md for details. Click <b>Connect</b> once pysim-otaman-server is ready.</p>
</div> </div>
<div id="pysim-connected-row" class="mb-3 hidden"> <div id="pysim-connected-row" class="mb-3 hidden">
<div class="flex items-center gap-2"> <div class="flex gap-3 items-start">
<span id="pysim-status-dot" class="inline-block w-3 h-3 rounded-full bg-green-500"></span> <img id="pysim-status-img" src="nosim.svg" alt="pysim" class="w-16 h-16 shrink-0 dark:invert">
<span class="text-sm font-medium">Connected</span> <div class="flex-1 min-w-0">
<button onclick="pysimEquipCmd()" 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 text-gray-700 dark:text-slate-300">Equip card</button> <div class="flex items-center gap-2 mb-1 flex-wrap">
<button onclick="pysimStatusCmd()" 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 text-gray-700 dark:text-slate-300">Check status</button> <button onclick="pysimEquipCmd()" 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 text-gray-700 dark:text-slate-300">Equip card</button>
<button onclick="pysimResetCmd()" 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 text-gray-700 dark:text-slate-300">Reset card</button> <button onclick="pysimStatusCmd()" 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 text-gray-700 dark:text-slate-300">Check status</button>
<button onclick="pysimDisconnect()" id="pysim-disconnect-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 text-gray-700 dark:text-slate-300">Disconnect from pySim</button> <button onclick="pysimResetCmd()" 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 text-gray-700 dark:text-slate-300">Reset card</button>
<button onclick="pysimDisconnect()" id="pysim-disconnect-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 text-gray-700 dark:text-slate-300">Disconnect from pySim</button>
</div>
<div id="pysim-status" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
</div>
</div> </div>
</div> </div>
<div id="pysim-status" class="mb-3 text-xs font-mono text-gray-600 dark:text-slate-400"></div> <div id="pysim-status" class="mb-3 text-xs font-mono text-gray-600 dark:text-slate-400"></div>
@@ -2436,9 +2440,13 @@ async function pysimFetch(path, body) {
return await res.json(); return await res.json();
} }
function pysimSetDot(ok) { function pysimSetConnected(state) {
const dot = document.getElementById('pysim-status-dot'); const img = document.getElementById('pysim-status-img');
dot.className = 'inline-block w-3 h-3 rounded-full ' + (ok ? 'bg-green-500' : 'bg-red-500'); if (state === 'spin') {
img.src = 'sim_anim.svg';
} else {
img.src = state ? 'sim.svg' : 'nosim.svg';
}
} }
function pysimShowConnected(connected) { function pysimShowConnected(connected) {
@@ -2451,7 +2459,7 @@ async function pysimRefresh() {
const statusEl = document.getElementById('pysim-status'); const statusEl = document.getElementById('pysim-status');
try { try {
const data = await pysimFetch('/api/status'); const data = await pysimFetch('/api/status');
pysimSetDot(true); pysimSetConnected(true);
let html = 'Reader: <b>' + esc(data.reader || 'none') + '</b>' + let html = 'Reader: <b>' + esc(data.reader || 'none') + '</b>' +
' | Card: <b>' + esc(data.card || 'none') + '</b>' + ' | Card: <b>' + esc(data.card || 'none') + '</b>' +
' | Profile: <b>' + esc(data.profile || 'none') + '</b><br>' + ' | Profile: <b>' + esc(data.profile || 'none') + '</b><br>' +
@@ -2475,7 +2483,7 @@ async function pysimRefresh() {
statusEl.innerHTML = html; statusEl.innerHTML = html;
return true; return true;
} catch (e) { } catch (e) {
pysimSetDot(false); pysimSetConnected(false);
statusEl.textContent = 'Connection failed: ' + e.message; statusEl.textContent = 'Connection failed: ' + e.message;
return false; return false;
} }
@@ -2489,6 +2497,7 @@ async function pysimConnect() {
btn.textContent = 'Connecting...'; btn.textContent = 'Connecting...';
const statusEl = document.getElementById('pysim-status'); const statusEl = document.getElementById('pysim-status');
statusEl.innerHTML = '<span class="text-gray-500">Checking card...</span>'; statusEl.innerHTML = '<span class="text-gray-500">Checking card...</span>';
pysimSetConnected('spin');
try { try {
const status = await pysimFetch('/api/status'); const status = await pysimFetch('/api/status');
if (!status.app_ready) { if (!status.app_ready) {
@@ -2496,21 +2505,21 @@ async function pysimConnect() {
} }
const cmd = await pysimFetch('/api/command', { cmd: 'status' }); const cmd = await pysimFetch('/api/command', { cmd: 'status' });
if (cmd.output && (cmd.output.includes('Exception') || cmd.output.includes('ProtocolError'))) { if (cmd.output && (cmd.output.includes('Exception') || cmd.output.includes('ProtocolError'))) {
pysimSetDot(false); pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>'; statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>';
pysimShowConnected(true); pysimShowConnected(true);
btn.textContent = 'Connected'; btn.textContent = 'Connected';
btn.disabled = false; btn.disabled = false;
return; return;
} }
pysimSetDot(true); pysimSetConnected(true);
pysimShowConnected(true); pysimShowConnected(true);
await pysimRefresh(); await pysimRefresh();
pysimLoadCommands(); pysimLoadCommands();
pysimFsRefresh(); pysimFsRefresh();
btn.textContent = 'Connected'; btn.textContent = 'Connected';
} catch (e) { } catch (e) {
pysimSetDot(false); pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">Connection failed: ' + esc(e.message) + '</span>'; statusEl.innerHTML = '<span class="text-red-500">Connection failed: ' + esc(e.message) + '</span>';
btn.textContent = 'Connect'; btn.textContent = 'Connect';
} }
@@ -2520,7 +2529,7 @@ async function pysimConnect() {
function pysimDisconnect() { function pysimDisconnect() {
pysimShowConnected(false); pysimShowConnected(false);
document.getElementById('pysim-status').textContent = ''; document.getElementById('pysim-status').textContent = '';
pysimSetDot(false); pysimSetConnected(false);
} }
async function pysimRunCmd(cmd) { async function pysimRunCmd(cmd) {
@@ -2528,13 +2537,13 @@ async function pysimRunCmd(cmd) {
try { try {
const result = await pysimFetch('/api/command', { cmd }); const result = await pysimFetch('/api/command', { cmd });
if (result.output && (result.output.includes('Exception') || result.output.includes('ProtocolError') || result.output.includes('SwMatchError'))) { if (result.output && (result.output.includes('Exception') || result.output.includes('ProtocolError') || result.output.includes('SwMatchError'))) {
pysimSetDot(false); pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>'; statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>';
return null; return null;
} }
return result; return result;
} catch (e) { } catch (e) {
pysimSetDot(false); pysimSetConnected(false);
statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>'; statusEl.innerHTML = '<span class="text-red-500">No card detected. Insert card and click Equip card button</span>';
return null; return null;
} }
@@ -2543,6 +2552,7 @@ async function pysimRunCmd(cmd) {
async function pysimStatusCmd() { async function pysimStatusCmd() {
const statusEl = document.getElementById('pysim-status'); const statusEl = document.getElementById('pysim-status');
statusEl.innerHTML = '<span class="text-gray-500">Checking...</span>'; statusEl.innerHTML = '<span class="text-gray-500">Checking...</span>';
pysimSetConnected('spin');
const result = await pysimRunCmd('status'); const result = await pysimRunCmd('status');
if (result) await pysimRefresh(); if (result) await pysimRefresh();
} }
@@ -2550,6 +2560,7 @@ async function pysimStatusCmd() {
async function pysimResetCmd() { async function pysimResetCmd() {
const statusEl = document.getElementById('pysim-status'); const statusEl = document.getElementById('pysim-status');
statusEl.innerHTML = '<span class="text-gray-500">Resetting...</span>'; statusEl.innerHTML = '<span class="text-gray-500">Resetting...</span>';
pysimSetConnected('spin');
const result = await pysimRunCmd('reset'); const result = await pysimRunCmd('reset');
if (result) { if (result) {
await pysimRefresh(); await pysimRefresh();
@@ -2560,6 +2571,7 @@ async function pysimResetCmd() {
async function pysimEquipCmd() { async function pysimEquipCmd() {
const statusEl = document.getElementById('pysim-status'); const statusEl = document.getElementById('pysim-status');
statusEl.innerHTML = '<span class="text-gray-500">Equipping...</span>'; statusEl.innerHTML = '<span class="text-gray-500">Equipping...</span>';
pysimSetConnected('spin');
const result = await pysimRunCmd('equip'); const result = await pysimRunCmd('equip');
if (result) { if (result) {
await pysimRefresh(); await pysimRefresh();
+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="183"
height="183"
viewBox="-56.5 -44 183 183"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
sodipodi:docname="nosim.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="3.8098056"
inkscape:cx="91.212002"
inkscape:cy="111.02929"
inkscape:window-width="1707"
inkscape:window-height="1007"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1" /><g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1"><path
style="fill:#000000"
d="m -0.1304947,103.98892 c -2.832044,-0.76743 -5.441699,-3.02673 -6.734761,-5.830592 l -0.600371,-1.30184 V 47.511687 -1.833103 l 0.690582,-1.45521 c 1.069347,-2.25335 2.620862,-3.8111129 4.935149,-4.9550229 l 2.01929,-0.9981 24.2233257,-0.07338 c 15.628147,-0.04735 24.667267,0.02165 25.474467,0.194438 2.93081,0.627379 3.24328,0.889045 14.83492,12.422955 9.75955,9.7109599 11.15853,11.2053299 11.90625,12.7181399 l 0.84727,1.71423 v 39.56077 39.560781 l -0.69058,1.4552 c -1.06939,2.253442 -2.62086,3.811122 -4.93526,4.955032 l -2.01939,0.9981 -34.27739,0.0512 c -29.0101883,0.0433 -34.4919167,-0.007 -35.6735017,-0.32711 z M 69.807477,99.400158 c 1.54279,-0.70076 2.94311,-2.44775 3.28556,-4.09897 0.19262,-0.92877 0.25873,-13.13398 0.21014,-38.793661 -0.0617,-32.61006 -0.12207,-37.5513 -0.46776,-38.31279 -0.58495,-1.28854 -21.30517,-21.96092 -22.62188,-22.56965 -1.00491,-0.46458 -2.26697,-0.48928 -25.003117,-0.48928 -22.6963917,0 -24.0007447,0.0254 -25.0178957,0.48742 -1.533296,0.69645 -2.941608,2.44785 -3.296415,4.09949004 -0.20721,0.96458 -0.267588,14.95269996 -0.209699,48.58271996 l 0.0813,47.228131 0.571082,1.05833 c 0.314094,0.58209 0.909407,1.37919 1.322916,1.77134 1.695763,1.60819 -0.536882,1.51482 36.3382887,1.51979 32.21189,0.004 33.78281,-0.0175 34.80748,-0.48287 z M 15.146229,88.018118 c -2.69069,-0.54101 -5.08953,-2.61032 -6.0729303,-5.23871 -0.53439,-1.42827 -0.54303,-1.82376 -0.47185,-21.602461 l 0.0725,-20.14755 0.77957,-1.40728 c 0.9491803,-1.71346 2.1625903,-2.81514 4.1031503,-3.72534 l 1.46728,-0.68821 h 19.976048 19.97604 l 1.46728,0.68821 c 1.94057,0.9102 3.15397,2.01188 4.10315,3.72534 l 0.77957,1.40728 0.0725,20.14755 c 0.0712,19.778701 0.0625,20.174191 -0.47184,21.602461 -0.72128,1.92779 -2.23869,3.6008 -4.13783,4.56212 l -1.54825,0.78371 -19.57917,0.0405 c -10.768537,0.0223 -20.000388,-0.0442 -20.515228,-0.14766 z m 9.535021,-10.07934 v -5.82084 h -5.820831 -5.82084 v 4.60991 4.60991 l 0.71449,0.84912 c 1.16572,1.38538 1.8679,1.55091 6.6277,1.56237 l 4.299481,0.0104 z m 16.139577,0.01 v -5.811 l -5.82107,-0.076 -5.821067,-0.076 2.4e-4,5.88698 2.4e-4,5.88698 h 5.820827 5.82083 z m 13.96991,5.43925 c 0.39296,-0.20445 1.01213,-0.68997 1.37593,-1.07893 0.64718,-0.69193 0.66325,-0.80958 0.7443,-5.4491 l 0.0829,-4.7419 -5.04378,-0.0331 c -2.77409,-0.0182 -5.22238,-0.0331 -5.44066,-0.0331 -1.24803,0 -1.19063,-0.28384 -1.19063,5.88698 v 5.82083 h 4.37877 c 3.24522,0 4.56372,-0.0962 5.09322,-0.37174 z m 2.16968,-21.720971 v -5.95312 H 41.470267 25.98011 l -0.64943,-0.64943 -0.64943,-0.64944 v -7.42036 -7.42036 h -4.241831 c -4.6105,0 -5.44266,0.18168 -6.60609,1.44224 l -0.66145,0.71669 -0.0734,12.94345 -0.0734,12.94346 h 21.967688 21.96769 z m -16.13959,-16.13974 v -5.95313 h -5.82083 -5.820827 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.716387,0.17638 5.820827,0.17638 h 5.64444 z m 16.13959,1.36204 c 0,-4.33264 -0.0338,-4.64169 -0.60059,-5.4901 -1.07012,-1.60187 -1.89152,-1.82419 -6.7416,-1.82465 l -4.29948,-4.2e-4 v 5.77674 c 0,3.17721 0.0794,5.85611 0.17639,5.95313 0.097,0.097 2.71639,0.17638 5.82083,0.17638 h 5.64445 z"
id="path1" /><rect
style="fill:#000000;stroke-width:1.37666"
id="rect1"
width="10.467919"
height="154.82217"
x="56.910221"
y="-74.725159"
transform="matrix(0.61811524,0.7860875,-0.79130094,0.61142687,0,0)" /></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+11 -20
View File
@@ -611,10 +611,6 @@ video {
display: block; display: block;
} }
.inline-block {
display: inline-block;
}
.flex { .flex {
display: flex; display: flex;
} }
@@ -627,8 +623,8 @@ video {
display: none; display: none;
} }
.h-3 { .h-16 {
height: 0.75rem; height: 4rem;
} }
.min-h-20 { .min-h-20 {
@@ -663,10 +659,6 @@ video {
width: 6rem; width: 6rem;
} }
.w-3 {
width: 0.75rem;
}
.w-6 { .w-6 {
width: 1.5rem; width: 1.5rem;
} }
@@ -679,6 +671,10 @@ video {
width: 100%; width: 100%;
} }
.min-w-0 {
min-width: 0px;
}
.max-w-4xl { .max-w-4xl {
max-width: 56rem; max-width: 56rem;
} }
@@ -832,21 +828,11 @@ video {
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
} }
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
}
.bg-neutral-50 { .bg-neutral-50 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(250 250 250 / var(--tw-bg-opacity, 1)); background-color: rgb(250 250 250 / var(--tw-bg-opacity, 1));
} }
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
}
.bg-transparent { .bg-transparent {
background-color: transparent; background-color: transparent;
} }
@@ -1174,6 +1160,11 @@ video {
color: rgb(250 204 21 / var(--tw-text-opacity, 1)); color: rgb(250 204 21 / var(--tw-text-opacity, 1));
} }
.dark\:invert:is(.dark *) {
--tw-invert: invert(100%);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.dark\:hover\:bg-slate-600:hover:is(.dark *) { .dark\:hover\:bg-slate-600:hover:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(71 85 105 / var(--tw-bg-opacity, 1)); background-color: rgb(71 85 105 / var(--tw-bg-opacity, 1));