feat: improve UF2 flow

This commit is contained in:
2024-11-03 14:39:35 +01:00
parent 06d122b5d6
commit b4605fe84d
11 changed files with 323 additions and 104 deletions

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { afterNavigate, beforeNavigate } from "$app/navigation";
import { expoIn, expoOut } from "svelte/easing";
import type { Snippet } from "svelte";
let { children, routeOrder }: { children: Snippet; routeOrder: string[]; direction: } =
$props();
let inDirection = $state(0);
let outDirection = $state(0);
let outroEnd: undefined | (() => void) = $state(undefined);
let animationDone: Promise<void>;
let isNavigating = $state(false);
function routeIndex(route: string | undefined): number {
return routeOrder.findIndex((it) => route?.startsWith(it));
}
beforeNavigate((navigation) => {
const from = routeIndex(navigation.from?.url.pathname);
const to = routeIndex(navigation.to?.url.pathname);
if (from === -1 || to === -1 || from === to) return;
isNavigating = true;
inDirection = from > to ? -1 : 1;
outDirection = from > to ? 1 : -1;
animationDone = new Promise((resolve) => {
outroEnd = resolve;
});
});
afterNavigate(async () => {
await animationDone;
isNavigating = false;
});
</script>
{#if !isNavigating}
<main
in:fly={{ y: inDirection * 24, duration: 150, easing: expoOut }}
out:fly={{ y: outDirection * 24, duration: 150, easing: expoIn }}
onoutroend={outroEnd}
>
{@render children()}
</main>
{/if}
<style lang="scss">
main {
padding: 0;
}
</style>

View File

@@ -53,11 +53,13 @@ export interface ProgressInfo {
}
export const syncProgress = writable<ProgressInfo | undefined>(undefined);
export async function initSerial(manual = false) {
export async function initSerial(manual = false, withSync = true) {
const device = get(serialPort) ?? new CharaDevice();
await device.init(manual);
serialPort.set(device);
await sync();
if (withSync) {
await sync();
}
}
export async function sync() {

View File

@@ -14,27 +14,26 @@
let isNavigating = $state(false);
const routeOrder = [
"/config/chords/",
"/config/layout/",
"/config/settings/",
"/config",
"/learn",
"/docs",
"/editor",
"/chat",
"/plugin",
];
function routeIndex(route: string | undefined): number {
return routeOrder.findIndex((it) => route?.startsWith(it));
}
beforeNavigate((navigation) => {
const from = navigation.from?.url.pathname;
const to = navigation.to?.url.pathname;
if (from === to) return;
const from = routeIndex(navigation.from?.url.pathname);
const to = routeIndex(navigation.to?.url.pathname);
if (from === -1 || to === -1 || from === to) return;
isNavigating = true;
if (!(from && to && routeOrder.includes(from) && routeOrder.includes(to))) {
inDirection = 0;
outDirection = 0;
} else {
const fromIndex = routeOrder.indexOf(from);
const toIndex = routeOrder.indexOf(to);
inDirection = fromIndex > toIndex ? -1 : 1;
outDirection = fromIndex > toIndex ? 1 : -1;
}
inDirection = from > to ? -1 : 1;
outDirection = from > to ? 1 : -1;
animationDone = new Promise((resolve) => {
outroEnd = resolve;
@@ -49,8 +48,8 @@
{#if !isNavigating}
<main
in:fly={{ x: inDirection * 24, duration: 150, easing: expoOut }}
out:fly={{ x: outDirection * 24, duration: 150, easing: expoIn }}
in:fly={{ y: inDirection * 24, duration: 150, easing: expoOut }}
out:fly={{ y: outDirection * 24, duration: 150, easing: expoIn }}
onoutroend={outroEnd}
>
{@render children()}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { Snippet } from "svelte";
import PageTransition from "./PageTransition.svelte";
import Navigation from "./Navigation.svelte";
let { children }: { children?: Snippet } = $props();
@@ -8,5 +9,9 @@
<Navigation />
{#if children}
{@render children()}
<PageTransition>
{#if children}
{@render children()}
{/if}
</PageTransition>
{/if}

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { afterNavigate, beforeNavigate } from "$app/navigation";
import { expoIn, expoOut } from "svelte/easing";
import type { Snippet } from "svelte";
let { children }: { children: Snippet } = $props();
let inDirection = $state(0);
let outDirection = $state(0);
let outroEnd: undefined | (() => void) = $state(undefined);
let animationDone: Promise<void>;
let isNavigating = $state(false);
const routeOrder = [
"/config/chords/",
"/config/layout/",
"/config/settings/",
];
beforeNavigate((navigation) => {
const from = navigation.from?.url.pathname;
const to = navigation.to?.url.pathname;
if (from === to) return;
isNavigating = true;
if (!(from && to && routeOrder.includes(from) && routeOrder.includes(to))) {
inDirection = 0;
outDirection = 0;
} else {
const fromIndex = routeOrder.indexOf(from);
const toIndex = routeOrder.indexOf(to);
inDirection = fromIndex > toIndex ? -1 : 1;
outDirection = fromIndex > toIndex ? 1 : -1;
}
animationDone = new Promise((resolve) => {
outroEnd = resolve;
});
});
afterNavigate(async () => {
await animationDone;
isNavigating = false;
});
</script>
{#if !isNavigating}
<main
in:fly={{ x: inDirection * 24, duration: 150, easing: expoOut }}
out:fly={{ x: outDirection * 24, duration: 150, easing: expoIn }}
onoutroend={outroEnd}
>
{@render children()}
</main>
{/if}
<style lang="scss">
main {
padding: 0;
width: 100%;
}
</style>

View File

@@ -315,7 +315,7 @@
}
input[type="checkbox"] {
font-size: 12px;
font-size: 12px !important;
}
fieldset {

View File

@@ -4,15 +4,6 @@
let { data } = $props();
let files: FileList | null = $state(null);
$effect(() => {
const file = files?.[0];
if (file && $serialPort) {
$serialPort.updateFirmware(file);
}
});
let currentDevice = $derived(
$serialPort
? `${$serialPort.device.toLowerCase()}_${$serialPort.chipset.toLowerCase()}`
@@ -34,8 +25,6 @@
<aside transition:slide>Connect your device to see which one you need</aside>
{/if}
<input type="file" accept=".bin" bind:files />
<style lang="scss">
ul {
display: flex;

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { serialPort } from "$lib/serial/connection";
import { slide } from "svelte/transition";
import { downloadBackup } from "$lib/backup/backup";
import { initSerial, serialPort } from "$lib/serial/connection";
import { fade, slide } from "svelte/transition";
let { data } = $props();
@@ -8,6 +9,8 @@
let success = $state(false);
let error = $state<Error | undefined>(undefined);
let step = $state(0);
async function update() {
working = true;
error = undefined;
@@ -59,6 +62,46 @@
return `${(value / 1024 / 1024).toFixed(2)}MB`;
}
}
async function connect() {
try {
await initSerial(true, false);
step = 1;
} catch (e) {
error = e as Error;
}
}
function backup() {
downloadBackup();
step = 2;
}
function bootloader() {
$serialPort?.bootloader();
$serialPort = undefined;
step = 3;
}
async function getFileSystem() {
if (!uf2Url) return;
const uf2Promise = fetch(uf2Url).then((it) => it.blob());
const handle = await window.showSaveFilePicker({
id: `${data.device}-update`,
suggestedName: "CURRENT.UF2",
excludeAcceptAllOption: true,
types: [
{
description: "UF2 Firmware",
accept: { "application/octet-stream": [".UF2"] },
},
],
});
const writable = await handle.createWritable();
const uf2 = await uf2Promise;
await uf2.stream().pipeTo(writable);
step = 4;
}
</script>
<div>
@@ -71,6 +114,44 @@
to <em class="version">{data.version}</em>
</h2>
{#if data.ota && !data.device.endsWith("m0")}
{@const buttonError = error || (!success && isCorrectDevice === false)}
<section>
<button
class="update-button"
class:working
class:primary={!buttonError}
class:error={buttonError}
disabled={working || $serialPort === undefined || !isCorrectDevice}
onclick={update}>Apply Update</button
>
{#if $serialPort && isCorrectDevice}
<div transition:slide>
Your device is ready and compatible. Click the button to perform the
update.
</div>
{:else if $serialPort && isCorrectDevice === false}
<div class="error" transition:slide>
Your device is incompatible with the selected update.
</div>
{:else if success}
<div class="primary" transition:slide>Update successful</div>
{:else if error}
<div class="error" transition:slide>{error.message}</div>
{:else if working}
<div class="primary" transition:slide>Updating your device...</div>
{:else}
<div class="primary" transition:slide>
Connect your device to continue
</div>
{/if}
</section>
<hr />
<h3>Manual Update</h3>
{/if}
<ul class="files">
{#if data.uf2}
<li>
@@ -99,59 +180,47 @@
{/if}
<section>
<h3>OTA Upate</h3>
{#if data.ota}
{@const buttonError = error || (!success && isCorrectDevice === false)}
<button
class:working
class:primary={!buttonError}
class:error={buttonError}
disabled={working || $serialPort === undefined || !isCorrectDevice}
onclick={update}>Apply Update</button
>
{#if $serialPort && isCorrectDevice}
<div transition:slide>
Your device is ready and compatible. Click the button to perform the
update.
</div>
{:else if $serialPort && isCorrectDevice === false}
<div class="error" transition:slide>
Your device is incompatible with the selected update.
</div>
{:else if success}
<div class="primary" transition:slide>Update successful</div>
{:else if error}
<div class="error" transition:slide>{error.message}</div>
{:else if working}
<div class="primary" transition:slide>Updating your device...</div>
{:else}
<div class="primary" transition:slide>
Connect your device to continue
</div>
{/if}
{:else}
<em>There are no OTA files for this device.</em>
{/if}
</section>
<hr />
<h3>Other options</h3>
<section>
<h4>Via UF2</h4>
<h4>UF2 Instructions</h4>
<ol>
<li>Backup your device</li>
<li>Reboot to bootloader</li>
<li>Save CURRENT.UF2 to the new drive</li>
<li>Restore</li>
<li>
<button class="inline-button" onclick={connect}
><span class="icon">usb</span>Connect</button
>
your device
{#if step >= 1}
<span class="icon ok" transition:fade>check_circle</span>
{/if}
</li>
<li class:faded={step < 1}>
Make a <button class="inline-button" onclick={backup}
><span class="icon">download</span>Backup</button
>
{#if step >= 2}
<span class="icon ok" transition:fade>check_circle</span>
{/if}
</li>
<li class:faded={step < 2}>
Reboot to <button class="inline-button" onclick={bootloader}
><span class="icon">restart_alt</span>Bootloader</button
>
{#if step >= 3}
<span class="icon ok" transition:fade>check_circle</span>
{/if}
</li>
<li class:faded={step < 3}>
Replace <button class="inline-button" onclick={getFileSystem}
><span class="icon">deployed_code_update</span>CURRENT.UF2</button
>
on the new drive
{#if step >= 4}
<span class="icon ok" transition:fade>check_circle</span>
{/if}
</li>
</ol>
</section>
<section>
<h4>Via Serial</h4>
<p>WIP</p>
</section>
ading 0 Chordmaps.
</div>
<style lang="scss">
@@ -189,7 +258,32 @@
}
}
button {
button.inline-button {
display: inline;
padding: 0;
margin: 0;
height: unset;
font-size: inherit;
color: var(--md-sys-color-primary);
.icon {
font-size: 1.2em;
translate: 0 0.1em;
padding-inline-end: 0.2em;
}
}
.icon.ok {
font-size: 1.2em;
translate: 0 0.1em;
--icon-fill: 1;
}
.faded {
opacity: 0.8;
}
button.update-button {
overflow: hidden;
position: relative;
height: 42px;
@@ -250,26 +344,26 @@
display: flex;
padding: 0;
gap: 8px;
}
a {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr;
border: 1px solid var(--md-sys-color-outline);
border-radius: 8px;
font-size: 0.9em;
height: auto;
a[download] {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr;
border: 1px solid var(--md-sys-color-outline);
border-radius: 8px;
font-size: 0.9em;
height: auto;
.size {
font-size: 0.8em;
opacity: 0.8;
}
.size {
font-size: 0.8em;
opacity: 0.8;
}
.icon {
padding-inline-start: 0.4em;
grid-column: 2;
grid-row: 1 / span 2;
}
.icon {
padding-inline-start: 0.4em;
grid-column: 2;
grid-row: 1 / span 2;
}
}