mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-05 01:22:50 +00:00
feat: improve UF2 flow
This commit is contained in:
55
src/lib/PageTransition.svelte
Normal file
55
src/lib/PageTransition.svelte
Normal 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>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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}
|
||||
|
||||
65
src/routes/(app)/config/PageTransition.svelte
Normal file
65
src/routes/(app)/config/PageTransition.svelte
Normal 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>
|
||||
@@ -315,7 +315,7 @@
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
font-size: 12px;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user