Add Svelte GUI, FIT encoder and README
Standalone binary embeds the frontend, avoiding the dev-server dependency that made the window fail to load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { app } from './lib/app.svelte';
|
||||
import { api, inTauri } from './lib/bridge';
|
||||
import ConnectionScreen from './components/ConnectionScreen.svelte';
|
||||
import HelpOverlay from './components/HelpOverlay.svelte';
|
||||
import ProfileDrawer from './components/ProfileDrawer.svelte';
|
||||
import RideScreen from './components/RideScreen.svelte';
|
||||
import Toasts from './components/Toasts.svelte';
|
||||
|
||||
let ready = $state(false);
|
||||
let fatal = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
if (!inTauri) {
|
||||
fatal = 'Not running inside the Tauri shell — start the app with `cargo tauri dev`.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await app.init();
|
||||
ready = true;
|
||||
} catch (e) {
|
||||
fatal = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Keyboard control (FR-3.19). Handled once, here, and dispatched straight to
|
||||
* Rust — the frontend never applies a step itself, it only asks.
|
||||
*/
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
|
||||
const step = e.shiftKey ? 2 : 0.5;
|
||||
const run = (fn: () => Promise<unknown>) => {
|
||||
e.preventDefault();
|
||||
app.run(fn);
|
||||
};
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
return run(() => api.nudgeGradient(step));
|
||||
case 'ArrowDown':
|
||||
return run(() => api.nudgeGradient(-step));
|
||||
case '0':
|
||||
return run(() => api.resetGradient());
|
||||
case ' ':
|
||||
return run(() => api.togglePause());
|
||||
case 'm':
|
||||
case 'M':
|
||||
return run(() => api.cycleMode());
|
||||
case 'l':
|
||||
case 'L':
|
||||
return run(() => api.markLap());
|
||||
case ']':
|
||||
return run(() => bumpTarget(1));
|
||||
case '[':
|
||||
return run(() => bumpTarget(-1));
|
||||
case 'p':
|
||||
case 'P':
|
||||
e.preventDefault();
|
||||
app.showProfiles = !app.showProfiles;
|
||||
return;
|
||||
case 'd':
|
||||
case 'D':
|
||||
app.screen = 'connect';
|
||||
return;
|
||||
case 'r':
|
||||
case 'R':
|
||||
app.screen = 'ride';
|
||||
return;
|
||||
case '?':
|
||||
app.showHelp = !app.showHelp;
|
||||
return;
|
||||
case 'Escape':
|
||||
app.showHelp = false;
|
||||
app.showProfiles = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** `[` / `]` adjust whichever target the active mode actually uses. */
|
||||
async function bumpTarget(dir: number): Promise<unknown> {
|
||||
const ride = app.ride;
|
||||
if (!ride) return;
|
||||
if (ride.mode === 'Erg') return api.setPower(ride.powerTargetW + dir * 10);
|
||||
if (ride.mode === 'Resistance') return api.setResistance(ride.resistanceLevel + dir * 2);
|
||||
return api.nudgeGradient(dir * 0.5);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKey} />
|
||||
|
||||
<main>
|
||||
{#if fatal}
|
||||
<div class="fatal">
|
||||
<h1>BikeControl could not start</h1>
|
||||
<p>{fatal}</p>
|
||||
</div>
|
||||
{:else if !ready}
|
||||
<div class="boot"><span class="label">Starting…</span></div>
|
||||
{:else if app.screen === 'ride'}
|
||||
<RideScreen />
|
||||
{:else}
|
||||
<ConnectionScreen />
|
||||
{/if}
|
||||
|
||||
{#if app.showProfiles}<ProfileDrawer />{/if}
|
||||
{#if app.showHelp}<HelpOverlay />{/if}
|
||||
<Toasts />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.boot,
|
||||
.fatal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.fatal h1 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.fatal p {
|
||||
margin: 0;
|
||||
color: var(--ink-dim);
|
||||
max-width: 40rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user