mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2025-12-13 06:16:16 +00:00
Compare commits
4 Commits
df3d8a16de
...
1f4604bcbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
1f4604bcbc
|
|||
|
68faf57a22
|
|||
|
1d976947e1
|
|||
|
ca8bfac3bc
|
40
.github/workflows/build.yml
vendored
40
.github/workflows/build.yml
vendored
@@ -1,13 +1,9 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
CI:
|
||||
build:
|
||||
name: 🔨🚀 Build and deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -25,10 +21,10 @@ jobs:
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- name: 🐉 Use Node.js 18.16.x
|
||||
- name: 🐉 Use Node.js 22.4.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.16.x
|
||||
node-version: 22.4.x
|
||||
cache: "pnpm"
|
||||
- name: ⏬ Install Node dependencies
|
||||
run: pnpm install
|
||||
@@ -38,17 +34,17 @@ jobs:
|
||||
- name: 🔨 Build site
|
||||
run: pnpm build
|
||||
|
||||
- name: 📦 Upload build artifacts
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
- name: Disable jekyll
|
||||
run: touch build/.nojekyll
|
||||
- name: Custom domain
|
||||
run: echo 'manager.charachorder.com' > build/CNAME
|
||||
- run: git config user.name github-actions
|
||||
- run: git config user.email github-actions@github.com
|
||||
- run: git --work-tree build add --all
|
||||
- run: git commit -m "Automatic Deploy action run by github-actions"
|
||||
- run: git push origin HEAD:gh-pages --force
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
install -m 600 -D /dev/null ~/.ssh/id_rsa
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||
|
||||
- name: Publish Stable
|
||||
if: ${{ github.ref == 'refs/heads/v*' }}
|
||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
|
||||
|
||||
- name: Publish Branch
|
||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${GITHUB_REF##*/}
|
||||
- name: Publish Commit
|
||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${{ github.sha }}
|
||||
|
||||
@@ -18,6 +18,8 @@ const config = {
|
||||
"update",
|
||||
"offline_pin",
|
||||
"warning",
|
||||
"dangerous",
|
||||
"check",
|
||||
"cable",
|
||||
"person",
|
||||
"sync",
|
||||
|
||||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@@ -15,6 +15,7 @@ interface ImportMetaEnv {
|
||||
readonly VITE_LATEST_FIRMWARE: string;
|
||||
readonly VITE_STORE_URL: string;
|
||||
readonly VITE_MATRIX_URL: string;
|
||||
readonly VITE_FIRMWARE_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -440,46 +440,80 @@ export class CharaDevice {
|
||||
return Number(await this.send(1, "RAM").then(([bytes]) => bytes));
|
||||
}
|
||||
|
||||
async updateFirmware(file: File): Promise<void> {
|
||||
const size = file.size;
|
||||
// use separate serial connection
|
||||
await this.port.open({ baudRate: this.baudRate });
|
||||
const decoderStream = new TextDecoderStream();
|
||||
this.port.readable!.pipeTo(decoderStream.writable);
|
||||
|
||||
const reader = decoderStream
|
||||
.readable!.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
||||
.getReader();
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "system",
|
||||
value: "Starting firmware update",
|
||||
});
|
||||
return it;
|
||||
});
|
||||
|
||||
const writer = this.port.writable!.getWriter();
|
||||
try {
|
||||
await writer.write(new TextEncoder().encode(`RST OTA\r\n`));
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
async updateFirmware(file: File | Blob): Promise<void> {
|
||||
while (this.lock) {
|
||||
await this.lock;
|
||||
}
|
||||
|
||||
console.log((await reader.read()).value);
|
||||
|
||||
await file.stream().pipeTo(this.port.writable!);
|
||||
|
||||
console.log((await reader.read()).value);
|
||||
|
||||
await reader.cancel();
|
||||
reader.releaseLock();
|
||||
await this.port.close();
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "system",
|
||||
value: "Success?",
|
||||
});
|
||||
return it;
|
||||
let resolveLock: (result: true) => void;
|
||||
this.lock = new Promise<true>((resolve) => {
|
||||
resolveLock = resolve;
|
||||
});
|
||||
try {
|
||||
if (this.suspendDebounceId) {
|
||||
clearTimeout(this.suspendDebounceId);
|
||||
} else {
|
||||
await this.wake();
|
||||
}
|
||||
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "system",
|
||||
value: "OTA Update",
|
||||
});
|
||||
return it;
|
||||
});
|
||||
|
||||
const writer = this.port.writable!.getWriter();
|
||||
try {
|
||||
await writer.write(new TextEncoder().encode(`RST OTA\r\n`));
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "input",
|
||||
value: "RST OTA",
|
||||
});
|
||||
return it;
|
||||
});
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
|
||||
// Wait for the device to be ready
|
||||
const signal = await this.reader.read();
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "output",
|
||||
value: signal.value!.trim(),
|
||||
});
|
||||
return it;
|
||||
});
|
||||
|
||||
await file.stream().pipeTo(this.port.writable!);
|
||||
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "input",
|
||||
value: `...${file.size} bytes`,
|
||||
});
|
||||
return it;
|
||||
});
|
||||
|
||||
const result = (await this.reader.read()).value!.trim();
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "output",
|
||||
value: result!,
|
||||
});
|
||||
return it;
|
||||
});
|
||||
|
||||
await this.suspend();
|
||||
|
||||
if (result !== "OTA OK") {
|
||||
throw new Error(result);
|
||||
}
|
||||
} finally {
|
||||
delete this.lock;
|
||||
resolveLock!(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import {
|
||||
themeBase,
|
||||
themeColor,
|
||||
themeSuccessBase,
|
||||
} from "$lib/style/theme.server";
|
||||
import type { LayoutServerLoad } from "./$types";
|
||||
|
||||
export const load = (async () => ({
|
||||
themeSuccessBase,
|
||||
themeBase,
|
||||
themeColor,
|
||||
})) satisfies LayoutServerLoad;
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { LayoutLoad } from "./$types";
|
||||
import { browser } from "$app/environment";
|
||||
import { charaFileFromUriComponent } from "$lib/share/share-url";
|
||||
import { themeBase, themeColor, themeSuccessBase } from "$lib/style/theme";
|
||||
|
||||
export const load = (async ({ url, data, fetch }) => {
|
||||
const importFile = browser && new URLSearchParams(url.search).get("import");
|
||||
return {
|
||||
...data,
|
||||
themeSuccessBase,
|
||||
themeBase,
|
||||
themeColor,
|
||||
importFile: importFile
|
||||
? await charaFileFromUriComponent(importFile, fetch)
|
||||
: undefined,
|
||||
|
||||
16
src/routes/(app)/ota-update/+layout.svelte
Normal file
16
src/routes/(app)/ota-update/+layout.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<h1><a href="/ota-update/">Firmware Update</a></h1>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<style lang="scss">
|
||||
h1 {
|
||||
margin-block: 1em;
|
||||
padding: 0;
|
||||
font-size: 3em;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { serialPort } from "$lib/serial/connection";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
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()}`
|
||||
: undefined,
|
||||
);
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each data.devices as device}
|
||||
<li>
|
||||
<a href="./{device.name}/" class:highlight={device.name === currentDevice}
|
||||
>{device.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if !currentDevice}
|
||||
<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;
|
||||
list-style: none;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
outline: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
background-color 200ms ease,
|
||||
color 200ms ease,
|
||||
outline-offset 200ms ease,
|
||||
outline-color 200ms ease;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
outline-offset: 0;
|
||||
}
|
||||
100% {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
scale: 1;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-5deg);
|
||||
scale: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
outline-width: 2px;
|
||||
outline-color: var(--md-sys-color-primary);
|
||||
animation: wiggle 500ms ease 2 alternate;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
</style>
|
||||
|
||||
9
src/routes/(app)/ota-update/+page.ts
Normal file
9
src/routes/(app)/ota-update/+page.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import type { DirectoryListing } from "./listing";
|
||||
|
||||
export const load = (async ({ fetch }) => {
|
||||
const result = await fetch(import.meta.env.VITE_FIRMWARE_URL);
|
||||
const data = await result.json();
|
||||
|
||||
return { devices: data as DirectoryListing[] };
|
||||
}) satisfies PageLoad;
|
||||
0
src/routes/(app)/ota-update/OTA.svelte
Normal file
0
src/routes/(app)/ota-update/OTA.svelte
Normal file
85
src/routes/(app)/ota-update/[device]/+page.svelte
Normal file
85
src/routes/(app)/ota-update/[device]/+page.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
|
||||
let showPrerelease = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="title">
|
||||
<h2>Versions available for <em>{data.device}</em></h2>
|
||||
<label
|
||||
>Include Pre-releases<input
|
||||
type="checkbox"
|
||||
bind:checked={showPrerelease}
|
||||
/></label
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if data.versions}
|
||||
<ul>
|
||||
{#each data.versions as version}
|
||||
{@const isPrerelease = version.name.includes("-")}
|
||||
<li class:pre-release={isPrerelease}>
|
||||
<a href="./{version.name}/"
|
||||
>{version.name}
|
||||
<time datetime={version.mtime}
|
||||
>{new Date(version.mtime).toLocaleDateString()}</time
|
||||
></a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<h2>The device {data.device} does not exist.</h2>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.pre-release {
|
||||
margin-inline-start: 2em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
height: 2em;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
height 200ms ease,
|
||||
opacity 200ms ease;
|
||||
}
|
||||
|
||||
label {
|
||||
padding: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
h2 {
|
||||
margin-block-end: 0;
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time {
|
||||
opacity: 0.5;
|
||||
&:before {
|
||||
content: "•";
|
||||
padding-inline: 0.4ch;
|
||||
}
|
||||
}
|
||||
|
||||
div.title:has(input:not(:checked)) ~ ul .pre-release {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
16
src/routes/(app)/ota-update/[device]/+page.ts
Normal file
16
src/routes/(app)/ota-update/[device]/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import type { DirectoryListing } from "../listing";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const result = await fetch(
|
||||
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/`,
|
||||
);
|
||||
const data = await result.json();
|
||||
|
||||
return {
|
||||
versions: (data as DirectoryListing[]).sort((a, b) =>
|
||||
b.name.localeCompare(a.name),
|
||||
),
|
||||
device: params.device,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
292
src/routes/(app)/ota-update/[device]/[version]/+page.svelte
Normal file
292
src/routes/(app)/ota-update/[device]/[version]/+page.svelte
Normal file
@@ -0,0 +1,292 @@
|
||||
<script lang="ts">
|
||||
import { serialPort } from "$lib/serial/connection";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let working = $state(false);
|
||||
let success = $state(false);
|
||||
let error = $state<Error | undefined>(undefined);
|
||||
|
||||
async function update() {
|
||||
working = true;
|
||||
error = undefined;
|
||||
success = false;
|
||||
const port = $serialPort!;
|
||||
$serialPort = undefined;
|
||||
try {
|
||||
const file = await fetch(otaUrl!).then((it) => it.blob());
|
||||
|
||||
await port.updateFirmware(file);
|
||||
|
||||
success = true;
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
} finally {
|
||||
working = false;
|
||||
}
|
||||
}
|
||||
|
||||
let currentDevice = $derived(
|
||||
$serialPort
|
||||
? `${$serialPort.device.toLowerCase()}_${$serialPort.chipset.toLowerCase()}`
|
||||
: undefined,
|
||||
);
|
||||
let isCorrectDevice = $derived(
|
||||
currentDevice ? currentDevice === data.device : undefined,
|
||||
);
|
||||
|
||||
let uf2Url = $derived(
|
||||
data.uf2
|
||||
? `${import.meta.env.VITE_FIRMWARE_URL}${data.device}/${data.version}/${data.uf2.name}`
|
||||
: undefined,
|
||||
);
|
||||
let otaUrl = $derived(
|
||||
data.ota
|
||||
? `${import.meta.env.VITE_FIRMWARE_URL}${data.device}/${data.version}/${data.ota.name}`
|
||||
: undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* Bytes to respective units
|
||||
*/
|
||||
function toByteUnit(value: number) {
|
||||
if (value < 1024) {
|
||||
return `${value}B`;
|
||||
} else if (value < 1024 * 1024) {
|
||||
return `${(value / 1024).toFixed(2)}KB`;
|
||||
} else {
|
||||
return `${(value / 1024 / 1024).toFixed(2)}MB`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h2>
|
||||
Update <em
|
||||
class="device"
|
||||
class:correct-device={isCorrectDevice === true}
|
||||
class:incorrect-device={isCorrectDevice === false}>{data.device}</em
|
||||
>
|
||||
to <em class="version">{data.version}</em>
|
||||
</h2>
|
||||
|
||||
<ul class="files">
|
||||
{#if data.uf2}
|
||||
<li>
|
||||
<a target="_blank" download href={uf2Url}
|
||||
>{data.uf2.name} <span class="icon">download</span><span class="size"
|
||||
>{toByteUnit(data.uf2.size)}</span
|
||||
></a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
{#if data.ota}
|
||||
<li>
|
||||
<a target="_blank" download href={otaUrl}
|
||||
>{data.ota.name} <span class="icon">download</span><span class="size"
|
||||
>{toByteUnit(data.uf2.size)}</span
|
||||
></a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
{#if isCorrectDevice === false}
|
||||
<div transition:slide class="incorrect-device">
|
||||
These files are incompatible with your device
|
||||
</div>
|
||||
{/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>
|
||||
<ol>
|
||||
<li>Backup your device</li>
|
||||
<li>Reboot to bootloader</li>
|
||||
<li>Save CURRENT.UF2 to the new drive</li>
|
||||
<li>Restore</li>
|
||||
</ol>
|
||||
</section>
|
||||
<section>
|
||||
<h4>Via Serial</h4>
|
||||
<p>WIP</p>
|
||||
</section>
|
||||
ading 0 Chordmaps.
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
h2 > em {
|
||||
font-style: normal;
|
||||
transition: color 200ms ease;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(120deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: rotate(120deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 42px;
|
||||
|
||||
border: 2px solid currentcolor;
|
||||
border-radius: 8px;
|
||||
|
||||
outline: 2px dashed currentcolor;
|
||||
outline-offset: 4px;
|
||||
|
||||
background: var(--md-sys-color-background);
|
||||
transition:
|
||||
border 200ms ease,
|
||||
color 200ms ease;
|
||||
|
||||
margin: 6px;
|
||||
margin-block: 16px;
|
||||
|
||||
&.primary {
|
||||
color: var(--md-sys-color-primary);
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.working {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.working::before {
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
background: var(--md-sys-color-background);
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
border-radius: 8px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&.working::after {
|
||||
z-index: -2;
|
||||
position: absolute;
|
||||
content: "";
|
||||
background: var(--md-sys-color-primary);
|
||||
animation: rotate 1s ease-out forwards infinite;
|
||||
height: 30%;
|
||||
width: 120%;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
color: var(--md-sys-color-outline);
|
||||
margin-block: 3em;
|
||||
margin-inline: 5em;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.files {
|
||||
list-style: none;
|
||||
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;
|
||||
|
||||
.size {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-inline-start: 0.4em;
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
color: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
.device {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.correct-device {
|
||||
color: var(--md-sys-color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.incorrect-device {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
</style>
|
||||
20
src/routes/(app)/ota-update/[device]/[version]/+page.ts
Normal file
20
src/routes/(app)/ota-update/[device]/[version]/+page.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import type { FileListing, Listing } from "../../listing";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const result = await fetch(
|
||||
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/`,
|
||||
);
|
||||
const data: Listing[] = await result.json();
|
||||
|
||||
return {
|
||||
uf2: data.find(
|
||||
(entry) => entry.type === "file" && entry.name === "CURRENT.UF2",
|
||||
) as FileListing,
|
||||
ota: data.find(
|
||||
(entry) => entry.type === "file" && entry.name === "firmware.bin",
|
||||
),
|
||||
version: params.version,
|
||||
device: params.device,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
14
src/routes/(app)/ota-update/listing.ts
Normal file
14
src/routes/(app)/ota-update/listing.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type Listing = FileListing | DirectoryListing;
|
||||
|
||||
export interface DirectoryListing {
|
||||
name: string;
|
||||
type: "directory";
|
||||
mtime: string;
|
||||
}
|
||||
|
||||
export interface FileListing {
|
||||
name: string;
|
||||
type: "file";
|
||||
mtime: string;
|
||||
size: number;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// noinspection ES6PreferShortImport
|
||||
import { themeColor } from "./src/lib/style/theme.server";
|
||||
import { themeColor } from "./src/lib/style/theme";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import { SvelteKitPWA } from "@vite-pwa/sveltekit";
|
||||
@@ -23,6 +23,7 @@ process.env["VITE_LEARN_URL"] = "https://www.iq-eq.io/";
|
||||
process.env["VITE_LATEST_FIRMWARE"] = "1.1.4";
|
||||
process.env["VITE_STORE_URL"] = "https://www.charachorder.com/";
|
||||
process.env["VITE_MATRIX_URL"] = "https://charachorder.io/";
|
||||
process.env["VITE_FIRMWARE_URL"] = "https://charachorder.io/firmware/";
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
@@ -54,10 +55,11 @@ export default defineConfig({
|
||||
workbox: {
|
||||
// https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#globpatterns
|
||||
globPatterns: [
|
||||
"client/**/*.{js,map,css,ico,woff2,csv,png,webp,svg,webmanifest}",
|
||||
"client/**/*.{js,css,ico,woff2,csv,png,webp,svg,webmanifest}",
|
||||
"prerendered/**/*.html",
|
||||
],
|
||||
ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/],
|
||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||
},
|
||||
manifest: {
|
||||
name: "CharaChorder Device Manager",
|
||||
|
||||
Reference in New Issue
Block a user