mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-04 09:02:50 +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">
|
<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();
|
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>
|
</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()}
|
{@render children()}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
h1 {
|
h1 {
|
||||||
|
display: flex;
|
||||||
margin-block: 1em;
|
margin-block: 1em;
|
||||||
padding: 0;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
let success = $state(false);
|
let success = $state(false);
|
||||||
let error = $state<Error | undefined>(undefined);
|
let error = $state<Error | undefined>(undefined);
|
||||||
|
|
||||||
|
let unsafeUpdate = $state(false);
|
||||||
|
|
||||||
let terminalOutput = $state("");
|
let terminalOutput = $state("");
|
||||||
|
|
||||||
let step = $state(0);
|
let step = $state(0);
|
||||||
@@ -187,17 +189,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<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")}
|
{#if data.meta.update.ota && !data.meta.target.endsWith("m0")}
|
||||||
{@const buttonError = error || (!success && isCorrectDevice === false)}
|
{@const buttonError = error || (!success && isCorrectDevice === false)}
|
||||||
<section>
|
<section>
|
||||||
@@ -237,87 +228,92 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h3>Manual Update</h3>
|
<label class="unsafe-opt-in"
|
||||||
|
><input type="checkbox" /> Unsafe recovery options</label
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isCorrectDevice === false}
|
<div class="unsafe-updates">
|
||||||
<div transition:slide class="incorrect-device">
|
{#if isCorrectDevice === false}
|
||||||
These files are incompatible with your device
|
<div transition:slide class="incorrect-device">
|
||||||
</div>
|
These files are incompatible with your device
|
||||||
{/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>
|
</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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -334,6 +330,20 @@
|
|||||||
overflow: auto;
|
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 {
|
.primary {
|
||||||
color: var(--md-sys-color-primary);
|
color: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
import type { FileListing, Listing } from "../../listing";
|
import type { FileListing, Listing } from "../../listing";
|
||||||
import type { VersionMeta } from "./meta";
|
import type { VersionMeta } from "$lib/meta";
|
||||||
|
|
||||||
export const load = (async ({ fetch, params }) => {
|
export const load = (async ({ fetch, params }) => {
|
||||||
const result = await fetch(
|
const result = await fetch(
|
||||||
@@ -23,7 +23,7 @@ export const load = (async ({ fetch, params }) => {
|
|||||||
git_commit: meta?.git_commit ?? "",
|
git_commit: meta?.git_commit ?? "",
|
||||||
git_is_dirty: meta?.git_is_dirty ?? false,
|
git_is_dirty: meta?.git_is_dirty ?? false,
|
||||||
git_date: meta?.git_date ?? data[0]?.mtime ?? "",
|
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,
|
development_mode: meta?.development_mode ?? 0,
|
||||||
update: {
|
update: {
|
||||||
uf2:
|
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