mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-04 12:07:23 +00:00
Add an On/Off/Reset bar above the screen
Buttons sit above the LCD as asked, with a state label that turns green when the guest is up. Powered off dims the panel via a screen-off class, so a dark screen is the signal rather than a frozen last frame. Off asks for confirmation: it ends the guest, and a stray click should not do that silently. Buttons disable while a power action is in flight, since On and Off take a couple of seconds and a double click would race. The stream is restarted after every power action. The old multipart response ends when the emulator goes away, so without a fresh src the image would stay blank after On. Refusals are surfaced in the status line rather than swallowed -- that is how the 409 for an adopted emulator becomes visible instead of looking like a dead button. 49 unit tests, and the generated page passes node --check.
This commit is contained in:
1 parent
699561794a
commit
6a9cf9f28a
2 files changed
+81
-2
No files matched your search
@@ -411,5 +411,31 @@ class TestKeyWhenPoweredOff(unittest.TestCase):
|
||||
self.assertEqual(resp.status_code, 409)
|
||||
|
||||
|
||||
class TestPowerBar(unittest.TestCase):
|
||||
def setUp(self):
|
||||
_, http = make_app()
|
||||
self.body = http.get("/").get_data(as_text=True)
|
||||
|
||||
def test_has_power_buttons(self):
|
||||
for action in ("on", "off", "reset"):
|
||||
self.assertIn(f'data-power="{action}"', self.body)
|
||||
|
||||
def test_power_bar_is_above_the_screen(self):
|
||||
"""The user asked for it on top."""
|
||||
self.assertLess(self.body.index('data-power="on"'),
|
||||
self.body.index('id="screen"'))
|
||||
|
||||
def test_power_off_asks_for_confirmation(self):
|
||||
"""Off kills the emulator; a stray click should not do that silently."""
|
||||
self.assertIn("confirm(", self.body)
|
||||
|
||||
def test_shows_the_powered_state(self):
|
||||
self.assertIn("powerstate", self.body)
|
||||
|
||||
def test_dims_the_screen_when_off(self):
|
||||
"""A dark screen is the signal that the machine is off."""
|
||||
self.assertIn("screen-off", self.body)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+55
-2
@@ -284,9 +284,24 @@ def render_index(scale: int) -> str:
|
||||
.side {{ width:76px; }}
|
||||
.hint {{ color:#6e7681; font-size:12px; text-align:center; max-width:430px; }}
|
||||
#status {{ font-size:12px; color:#6e7681; }}
|
||||
.powerbar {{ display:flex; gap:8px; align-items:center; align-self:stretch; }}
|
||||
.pwr {{ padding:6px 14px; font:inherit; color:#c9d1d9; background:#2b3138;
|
||||
border:1px solid #3a424b; border-radius:6px; cursor:pointer; }}
|
||||
.pwr:hover:not(:disabled) {{ background:#343b44; }}
|
||||
.pwr:disabled {{ opacity:0.5; cursor:default; }}
|
||||
#powerstate {{ font-size:12px; color:#6e7681; margin-left:auto; }}
|
||||
#powerstate.on {{ color:#3fb950; }}
|
||||
/* Powered off is a dark panel, not a frozen last frame. */
|
||||
.screen-off {{ background:#0b0d10 !important; }}
|
||||
</style>
|
||||
</head><body>
|
||||
<div class="radio">
|
||||
<div class="powerbar">
|
||||
<button class="pwr" data-power="on">On</button>
|
||||
<button class="pwr" data-power="off">Off</button>
|
||||
<button class="pwr" data-power="reset">Reset</button>
|
||||
<span id="powerstate">-</span>
|
||||
</div>
|
||||
<img id="screen" src="/stream" alt="radio LCD"
|
||||
width="{128 * scale}" height="{64 * scale}">
|
||||
<div class="body">
|
||||
@@ -366,14 +381,52 @@ addEventListener('blur', () => {{
|
||||
fetch('/api/release-all', {{method: 'POST'}}).catch(() => {{}});
|
||||
}});
|
||||
|
||||
document.querySelectorAll('.pwr').forEach(btn => {{
|
||||
btn.addEventListener('click', async () => {{
|
||||
const action = btn.dataset.power;
|
||||
// Off ends the guest. A stray click should not do that silently.
|
||||
if (action === 'off' &&
|
||||
!confirm('Power off the emulator? Guest state is lost.')) {{
|
||||
return;
|
||||
}}
|
||||
document.querySelectorAll('.pwr').forEach(b => b.disabled = true);
|
||||
try {{
|
||||
const r = await fetch('/api/power/' + action, {{method: 'POST'}});
|
||||
if (!r.ok) {{
|
||||
const j = await r.json().catch(() => ({{}}));
|
||||
document.getElementById('status').textContent =
|
||||
'power ' + action + ' refused: ' + (j.error || r.status);
|
||||
}}
|
||||
}} catch (err) {{
|
||||
document.getElementById('status').textContent = 'power failed: ' + err;
|
||||
}} finally {{
|
||||
document.querySelectorAll('.pwr').forEach(b => b.disabled = false);
|
||||
poll();
|
||||
// Restart the stream: the old one ends when the emulator goes away.
|
||||
const img = document.getElementById('screen');
|
||||
img.src = '/stream?t=' + Date.now();
|
||||
}}
|
||||
}});
|
||||
}});
|
||||
|
||||
function showPower(powered) {{
|
||||
const label = document.getElementById('powerstate');
|
||||
label.textContent = powered ? 'on' : 'off';
|
||||
label.classList.toggle('on', powered);
|
||||
document.getElementById('screen').classList.toggle('screen-off', !powered);
|
||||
}}
|
||||
|
||||
async function poll() {{
|
||||
try {{
|
||||
const r = await fetch('/api/status');
|
||||
const s = await r.json();
|
||||
showPower(!!s.powered);
|
||||
document.getElementById('status').textContent =
|
||||
'guest: ' + (s.status || 'unknown');
|
||||
s.powered ? ('guest: ' + (s.status || 'unknown'))
|
||||
: 'powered off -- press On to boot';
|
||||
}} catch (err) {{
|
||||
document.getElementById('status').textContent = 'emulator unreachable';
|
||||
showPower(false);
|
||||
document.getElementById('status').textContent = 'server unreachable';
|
||||
}}
|
||||
}}
|
||||
poll();
|
||||
|
||||
Reference in new issue
Block a user