mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2025-12-12 22:06:18 +00:00
feat: better update page
feat: hide manual update steps as "unsafe" if OTA is available resolves #155
This commit is contained in:
51
src/lib/components/AnimatedNumber.svelte
Normal file
51
src/lib/components/AnimatedNumber.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { fade, slide } from "svelte/transition";
|
||||
|
||||
let { value }: { value: number } = $props();
|
||||
|
||||
let digits: number[] = $derived(value.toString().split("").map(Number));
|
||||
const nums = Array.from({ length: 10 }, (_, i) => i);
|
||||
</script>
|
||||
|
||||
<div class="digits" style:width="{digits.length}ch">
|
||||
{#each digits as digit, i (digits.length - i)}
|
||||
<div
|
||||
class="digit-wrapper"
|
||||
style:right="{digits.length - 1 - i}ch"
|
||||
transition:fade
|
||||
>
|
||||
{#each nums as num (num)}
|
||||
<div
|
||||
class="digit"
|
||||
style:transform="translateY({(digit - num) / 4}em)"
|
||||
style:opacity={digit === num ? 1 : 0}
|
||||
>
|
||||
{num}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.digits {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transition: width 500ms ease;
|
||||
}
|
||||
|
||||
.digit-wrapper {
|
||||
display: inline-grid;
|
||||
height: 1em;
|
||||
width: 1ch;
|
||||
}
|
||||
|
||||
.digit {
|
||||
display: inline-block;
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
transition:
|
||||
transform 500ms ease,
|
||||
opacity 500ms ease;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,107 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import { serialPort } from "$lib/serial/connection";
|
||||
import { expoOut } from "svelte/easing";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const animationDuration = 400;
|
||||
const stagger = 80;
|
||||
|
||||
let targetDevice = $derived($page.params["device"]);
|
||||
let version = $derived($page.params["version"]);
|
||||
|
||||
let currentDevice = $derived(
|
||||
$serialPort
|
||||
? `${$serialPort.device.toLowerCase()}_${$serialPort.chipset.toLowerCase()}`
|
||||
: undefined,
|
||||
);
|
||||
let isCorrectDevice = $derived(
|
||||
currentDevice ? currentDevice === targetDevice : undefined,
|
||||
);
|
||||
|
||||
let fullBack = $state(false);
|
||||
|
||||
beforeNavigate(({ from, to, cancel }) => {
|
||||
fullBack = version !== undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1><a href="/ccos">Firmware Updates</a></h1>
|
||||
<h1>
|
||||
<a class="inline-link" href="/ccos">CCOS</a>
|
||||
{#if targetDevice !== undefined}
|
||||
<div
|
||||
class="uri-fragment"
|
||||
transition:slide={{
|
||||
axis: "x",
|
||||
duration: animationDuration,
|
||||
delay: fullBack ? stagger : 0,
|
||||
easing: expoOut,
|
||||
}}
|
||||
>
|
||||
<span class="separator">/</span>
|
||||
<a
|
||||
href="/ccos/{targetDevice}"
|
||||
class="device inline-link"
|
||||
class:correct-device={isCorrectDevice === true}
|
||||
class:incorrect-device={isCorrectDevice === false}>{targetDevice}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if version !== undefined}
|
||||
<div
|
||||
class="uri-fragment"
|
||||
transition:slide={{
|
||||
axis: "x",
|
||||
duration: animationDuration,
|
||||
easing: expoOut,
|
||||
}}
|
||||
>
|
||||
<span class="separator">/</span>
|
||||
<em class="version">{version}</em>
|
||||
</div>
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<style lang="scss">
|
||||
h1 {
|
||||
display: flex;
|
||||
margin-block: 1em;
|
||||
padding: 0;
|
||||
font-size: 3em;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.uri-fragment {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-inline: 0.5em;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
.device {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.inline-link {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.correct-device {
|
||||
color: var(--md-sys-color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.incorrect-device {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
let success = $state(false);
|
||||
let error = $state<Error | undefined>(undefined);
|
||||
|
||||
let unsafeUpdate = $state(false);
|
||||
|
||||
let terminalOutput = $state("");
|
||||
|
||||
let step = $state(0);
|
||||
@@ -187,17 +189,6 @@
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h2>
|
||||
<a class="inline-link" href="/ccos">CCOS</a> /
|
||||
<a
|
||||
href="/ccos/{data.meta.target}"
|
||||
class="device inline-link"
|
||||
class:correct-device={isCorrectDevice === true}
|
||||
class:incorrect-device={isCorrectDevice === false}>{data.meta.target}</a
|
||||
>
|
||||
/ <em class="version">{data.meta.version}</em>
|
||||
</h2>
|
||||
|
||||
{#if data.meta.update.ota && !data.meta.target.endsWith("m0")}
|
||||
{@const buttonError = error || (!success && isCorrectDevice === false)}
|
||||
<section>
|
||||
@@ -237,87 +228,92 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<h3>Manual Update</h3>
|
||||
<label class="unsafe-opt-in"
|
||||
><input type="checkbox" /> Unsafe recovery options</label
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if isCorrectDevice === false}
|
||||
<div transition:slide class="incorrect-device">
|
||||
These files are incompatible with your device
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section>
|
||||
<ol>
|
||||
<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>
|
||||
|
||||
{#if data.meta.update.esptool}
|
||||
<section>
|
||||
<h3>Factory Flash (WIP)</h3>
|
||||
<p>
|
||||
If everything else fails, you can go through the same process that is
|
||||
being used in the factory.
|
||||
</p>
|
||||
<p>
|
||||
This will temporarily brick your device if the process is not done
|
||||
completely or incorrectly.
|
||||
</p>
|
||||
|
||||
<div class="esp-buttons">
|
||||
<button onclick={espBootloader}
|
||||
><span class="icon">memory</span>ESP Bootloader</button
|
||||
>
|
||||
<button onclick={flashImages}
|
||||
><span class="icon">developer_board</span>Flash Images</button
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" id="erase" bind:checked={eraseAll} />Erase All</label
|
||||
>
|
||||
<button onclick={eraseSPI}
|
||||
><span class="icon">developer_board</span>Erase SPI Flash</button
|
||||
>
|
||||
<div class="unsafe-updates">
|
||||
{#if isCorrectDevice === false}
|
||||
<div transition:slide class="incorrect-device">
|
||||
These files are incompatible with your device
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<pre>{terminalOutput}</pre>
|
||||
<section>
|
||||
<ol>
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#if data.meta.update.esptool}
|
||||
<section>
|
||||
<h3>Factory Flash (WIP)</h3>
|
||||
<p>
|
||||
If everything else fails, you can go through the same process that is
|
||||
being used in the factory.
|
||||
</p>
|
||||
<p>
|
||||
This will temporarily brick your device if the process is not done
|
||||
completely or incorrectly.
|
||||
</p>
|
||||
|
||||
<div class="esp-buttons">
|
||||
<button onclick={espBootloader}
|
||||
><span class="icon">memory</span>ESP Bootloader</button
|
||||
>
|
||||
<button onclick={flashImages}
|
||||
><span class="icon">developer_board</span>Flash Images</button
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" id="erase" bind:checked={eraseAll} />Erase
|
||||
All</label
|
||||
>
|
||||
<button onclick={eraseSPI}
|
||||
><span class="icon">developer_board</span>Erase SPI Flash</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<pre>{terminalOutput}</pre>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -334,6 +330,20 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.unsafe-opt-in {
|
||||
margin-block: 1em;
|
||||
opacity: 0.6;
|
||||
font-size: 0.7em;
|
||||
|
||||
& + .unsafe-updates {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has(input:checked) + .unsafe-updates {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import type { FileListing, Listing } from "../../listing";
|
||||
import type { VersionMeta } from "./meta";
|
||||
import type { VersionMeta } from "$lib/meta";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const result = await fetch(
|
||||
@@ -23,7 +23,7 @@ export const load = (async ({ fetch, params }) => {
|
||||
git_commit: meta?.git_commit ?? "",
|
||||
git_is_dirty: meta?.git_is_dirty ?? false,
|
||||
git_date: meta?.git_date ?? data[0]?.mtime ?? "",
|
||||
public_build: meta?.public_build ?? !params.version.startsWith("."),
|
||||
public_build: meta?.public_build ?? !params.version.includes("+"),
|
||||
development_mode: meta?.development_mode ?? 0,
|
||||
update: {
|
||||
uf2:
|
||||
|
||||
20
src/routes/(app)/test/+page.svelte
Normal file
20
src/routes/(app)/test/+page.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import AnimatedNumber from "$lib/components/AnimatedNumber.svelte";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
let value = $state(Math.round(Math.random() * 100));
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(() => {
|
||||
value = Math.round(Math.random() * 100);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>The number is <AnimatedNumber {value} /></p>
|
||||
Reference in New Issue
Block a user