mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-19 00:13:01 +00:00
Compare commits
34 Commits
df3d8a16de
...
v2.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
f69be14b5e
|
|||
|
dce554fc66
|
|||
|
f152dbdcf5
|
|||
|
6a29e6a2fc
|
|||
|
9bf3801fef
|
|||
|
d2accfb838
|
|||
|
b8a376b93b
|
|||
|
588719df91
|
|||
|
6a0dad9dad
|
|||
|
f3704e4051
|
|||
|
3e6298717e
|
|||
|
aced0bbbb7
|
|||
|
|
36874c59e3 | ||
|
9dc61a3482
|
|||
|
d9183f952a
|
|||
|
913a833824
|
|||
|
0d6ef4d011
|
|||
|
232045964c
|
|||
|
3659b80e41
|
|||
|
3a02caeb6d
|
|||
|
259fd3a989
|
|||
|
dcf1d89fa0
|
|||
|
c79237ce22
|
|||
|
d68f1b19fa
|
|||
|
9cb36662b3
|
|||
|
b4605fe84d
|
|||
|
06d122b5d6
|
|||
|
3d25b030c6
|
|||
|
bf490ba823
|
|||
|
397f4bb6a9
|
|||
|
1f4604bcbc
|
|||
|
68faf57a22
|
|||
|
1d976947e1
|
|||
|
ca8bfac3bc
|
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
- name: 🐉 Use Node.js 22.4.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -41,16 +41,10 @@ jobs:
|
||||
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||
|
||||
- name: Publish Stable
|
||||
if: ${{ github.ref == 'ref/head/v*' }}
|
||||
if: ${{ github.ref == 'refs/tags/v*' }}
|
||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
|
||||
|
||||
- name: Publish Dev
|
||||
if: ${{ github.ref == 'ref/head/main' }}
|
||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/dev/
|
||||
|
||||
- name: Publish Tag
|
||||
if: ${{ github.ref == 'ref/head/v*' }}
|
||||
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${GITHUB_REF##*/v}
|
||||
|
||||
- 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 }}
|
||||
|
||||
@@ -4,6 +4,7 @@ const config = {
|
||||
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
||||
outputPath: "src/lib/assets/icons.min.woff2",
|
||||
icons: [
|
||||
"deployed_code_update",
|
||||
"adjust",
|
||||
"add",
|
||||
"piano",
|
||||
@@ -109,6 +110,9 @@ const config = {
|
||||
"experiment",
|
||||
"code",
|
||||
"dictionary",
|
||||
"developer_board",
|
||||
"developer_board_off",
|
||||
"memory",
|
||||
],
|
||||
codePoints: {
|
||||
speed: "e9e4",
|
||||
|
||||
69
package.json
69
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "charachorder-device-manager",
|
||||
"version": "1.5.2",
|
||||
"version": "2.2.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.16",
|
||||
"pnpm": ">=8.6"
|
||||
"node": ">=22.4",
|
||||
"pnpm": ">=9.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -34,61 +34,64 @@
|
||||
"typesafe-i18n": "typesafe-i18n"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/autocomplete": "^6.18.2",
|
||||
"@codemirror/commands": "^6.7.1",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/language": "^6.10.2",
|
||||
"@codemirror/language": "^6.10.3",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.29.1",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.36",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.1.3",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.1.0",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@melt-ui/svelte": "^0.86.0",
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.5.18",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.7.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@tauri-apps/cli": "^1.6.0",
|
||||
"@types/dom-view-transitions": "^1.0.5",
|
||||
"@types/flexsearch": "^0.7.6",
|
||||
"@types/w3c-web-serial": "^1.0.6",
|
||||
"@types/w3c-web-serial": "^1.0.7",
|
||||
"@types/w3c-web-usb": "^1.0.10",
|
||||
"@vite-pwa/sveltekit": "^0.6.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"codemirror": "^6.0.1",
|
||||
"cypress": "^13.13.2",
|
||||
"d3": "^7.9.0",
|
||||
"esptool-js": "^0.4.7",
|
||||
"flexsearch": "^0.7.43",
|
||||
"fontkit": "^2.0.2",
|
||||
"fontkit": "^2.0.4",
|
||||
"glob": "^11.0.0",
|
||||
"jsdom": "^24.1.1",
|
||||
"matrix-js-sdk": "^34.4.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"matrix-js-sdk": "^34.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"rxjs": "^7.8.1",
|
||||
"sass": "^1.77.8",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stylelint": "^16.8.1",
|
||||
"sass": "^1.80.6",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"stylelint": "^16.10.0",
|
||||
"stylelint-config-clean-order": "^6.1.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-prettier-scss": "^1.0.0",
|
||||
"stylelint-config-recommended-scss": "^14.1.0",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"svelte": "5.0.0-next.221",
|
||||
"svelte-check": "^3.8.5",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"svelte": "5.1.9",
|
||||
"svelte-check": "^4.0.5",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"tippy.js": "^6.3.7",
|
||||
"typesafe-i18n": "^5.26.2",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-mkcert": "^1.17.5",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vitest": "^2.0.5",
|
||||
"workbox-window": "^7.1.0"
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-mkcert": "^1.17.6",
|
||||
"vite-plugin-pwa": "^0.20.5",
|
||||
"vitest": "^2.1.4",
|
||||
"web-serial-polyfill": "^1.0.15",
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
1860
pnpm-lock.yaml
generated
1860
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "1.5.2"
|
||||
version = "2.2.1"
|
||||
description = "A Tauri App"
|
||||
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
||||
license = "AGPL-3"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": { "productName": "amacc1ng", "version": "1.5.2" },
|
||||
"package": { "productName": "amacc1ng", "version": "2.2.1" },
|
||||
"tauri": {
|
||||
"allowlist": { "all": false },
|
||||
"bundle": {
|
||||
|
||||
@@ -17,11 +17,11 @@ const de = {
|
||||
RELOAD: "Neu laden",
|
||||
},
|
||||
backup: {
|
||||
TITLE: "Lokale Kopie",
|
||||
INDIVIDUAL: "Einzeldateien",
|
||||
TITLE: "Backup",
|
||||
AUTO_BACKUP: "Auto-backup",
|
||||
DISCLAIMER:
|
||||
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
|
||||
DOWNLOAD: "Alles herunterladen",
|
||||
DOWNLOAD: "Alles",
|
||||
RESTORE: "Wiederherstellen",
|
||||
},
|
||||
modal: {
|
||||
@@ -109,7 +109,7 @@ const de = {
|
||||
},
|
||||
configure: {
|
||||
chords: {
|
||||
TITLE: "Akkorde",
|
||||
TITLE: "Bibliothek",
|
||||
HOLD_KEYS: "Akkord halten",
|
||||
NEW_CHORD: "Neuer Akkord",
|
||||
DUPLICATE: "Akkord existiert bereits",
|
||||
@@ -131,7 +131,7 @@ const de = {
|
||||
TITLE: "Layout",
|
||||
},
|
||||
settings: {
|
||||
TITLE: "Einstellungen",
|
||||
TITLE: "Gerät",
|
||||
},
|
||||
},
|
||||
plugin: {
|
||||
|
||||
@@ -13,11 +13,11 @@ const en = {
|
||||
TITLE: "Update your device",
|
||||
},
|
||||
backup: {
|
||||
TITLE: "Local backup",
|
||||
INDIVIDUAL: "Individual backups",
|
||||
TITLE: "Backup",
|
||||
AUTO_BACKUP: "Auto-backup",
|
||||
DISCLAIMER:
|
||||
"A backup is made and stored in this browser, and always remains only on your computer.",
|
||||
DOWNLOAD: "Download Everything",
|
||||
"Whenever you connect this device to browser, a backup is made locally and kept only on your computer.",
|
||||
DOWNLOAD: "Everything",
|
||||
RESTORE: "Restore",
|
||||
},
|
||||
sync: {
|
||||
@@ -108,7 +108,7 @@ const en = {
|
||||
},
|
||||
configure: {
|
||||
chords: {
|
||||
TITLE: "Chords",
|
||||
TITLE: "Library",
|
||||
HOLD_KEYS: "Hold chord",
|
||||
NEW_CHORD: "New chord",
|
||||
DUPLICATE: "Chord already exists",
|
||||
@@ -130,7 +130,7 @@ const en = {
|
||||
TITLE: "Layout",
|
||||
},
|
||||
settings: {
|
||||
TITLE: "Settings",
|
||||
TITLE: "Device",
|
||||
},
|
||||
},
|
||||
plugin: {
|
||||
|
||||
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>
|
||||
@@ -3,35 +3,35 @@ col:
|
||||
# Ring / Middle
|
||||
- offset: [2, 0]
|
||||
row:
|
||||
- switch: { d: 25, e: 26, n: 27, w: 28, s: 29 }
|
||||
- switch: { d: 20, e: 21, n: 22, w: 23, s: 24 }
|
||||
- switch: { e: 26, n: 27, w: 28, s: 29 }
|
||||
- switch: { e: 21, n: 22, w: 23, s: 24 }
|
||||
- offset: [4, 0]
|
||||
switch: { d: 65, w: 66, n: 67, e: 68, s: 69 }
|
||||
- switch: { d: 70, w: 71, n: 72, e: 73, s: 74 }
|
||||
switch: { w: 66, n: 67, e: 68, s: 69 }
|
||||
- switch: { w: 71, n: 72, e: 73, s: 74 }
|
||||
- offset: [2, 0]
|
||||
row:
|
||||
- switch: { d: 40, e: 41, n: 42, w: 43, s: 44 }
|
||||
- switch: { d: 35, e: 36, n: 37, w: 38, s: 39 }
|
||||
- switch: { e: 41, n: 42, w: 43, s: 44 }
|
||||
- switch: { e: 36, n: 37, w: 38, s: 39 }
|
||||
- offset: [4, 0]
|
||||
switch: { d: 80, w: 81, n: 82, e: 83, s: 84 }
|
||||
- switch: { d: 85, w: 86, n: 87, e: 88, s: 89 }
|
||||
switch: { w: 81, n: 82, e: 83, s: 84 }
|
||||
- switch: { w: 86, n: 87, e: 88, s: 89 }
|
||||
# Pinkie / Index
|
||||
- offset: [0, -3]
|
||||
row:
|
||||
- switch: { d: 30, e: 31, n: 32, w: 33, s: 34 }
|
||||
- switch: { e: 31, n: 32, w: 33, s: 34 }
|
||||
- offset: [4, 0]
|
||||
switch: { d: 15, e: 16, n: 17, w: 18, s: 19 }
|
||||
- switch: { d: 60, w: 61, n: 62, e: 63, s: 64 }
|
||||
switch: { e: 16, n: 17, w: 18, s: 19 }
|
||||
- switch: { w: 61, n: 62, e: 63, s: 64 }
|
||||
- offset: [4, 0]
|
||||
switch: { d: 75, w: 76, n: 77, e: 78, s: 79 }
|
||||
switch: { w: 76, n: 77, e: 78, s: 79 }
|
||||
# Thumbs
|
||||
- row:
|
||||
- offset: [5.5, 0.5]
|
||||
switch: { d: 10, e: 11, n: 12, w: 13, s: 14 }
|
||||
switch: { e: 11, n: 12, w: 13, s: 14 }
|
||||
- offset: [1, 0.5]
|
||||
switch: { d: 55, w: 56, n: 57, e: 58, s: 59 }
|
||||
switch: { w: 56, n: 57, e: 58, s: 59 }
|
||||
- row:
|
||||
- offset: [4.5, -0.25]
|
||||
switch: { d: 5, e: 6, n: 7, w: 8, s: 9 }
|
||||
switch: { e: 6, n: 7, w: 8, s: 9 }
|
||||
- offset: [3, -0.25]
|
||||
switch: { d: 50, w: 51, n: 52, e: 53, s: 54 }
|
||||
switch: { w: 51, n: 52, e: 53, s: 54 }
|
||||
|
||||
37
src/lib/assets/layouts/m4gr.yml
Normal file
37
src/lib/assets/layouts/m4gr.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: M4G
|
||||
col:
|
||||
# Ring / Middle
|
||||
- offset: [2, 0]
|
||||
row:
|
||||
- switch: { e: 26, n: 27, w: 28, s: 29 }
|
||||
- switch: { e: 21, n: 22, w: 23, s: 24 }
|
||||
- offset: [4, 0]
|
||||
switch: { w: 66, n: 67, e: 68, s: 69 }
|
||||
- switch: { w: 71, n: 72, e: 73, s: 74 }
|
||||
- offset: [2, 0]
|
||||
row:
|
||||
- switch: { e: 41, n: 42, w: 43, s: 44 }
|
||||
- switch: { e: 36, n: 37, w: 38, s: 39 }
|
||||
- offset: [4, 0]
|
||||
switch: { w: 81, n: 82, e: 83, s: 84 }
|
||||
- switch: { w: 86, n: 87, e: 88, s: 89 }
|
||||
# Pinkie / Index
|
||||
- offset: [0, -3]
|
||||
row:
|
||||
- switch: { e: 31, n: 32, w: 33, s: 34 }
|
||||
- offset: [4, 0]
|
||||
switch: { e: 16, n: 17, w: 18, s: 19 }
|
||||
- switch: { w: 61, n: 62, e: 63, s: 64 }
|
||||
- offset: [4, 0]
|
||||
switch: { w: 76, n: 77, e: 78, s: 79 }
|
||||
# Thumbs
|
||||
- row:
|
||||
- offset: [5.5, 0.5]
|
||||
switch: { e: 11, n: 12, w: 13, s: 14 }
|
||||
- offset: [1, 0.5]
|
||||
switch: { w: 56, n: 57, e: 58, s: 59 }
|
||||
- row:
|
||||
- offset: [4.5, -0.25]
|
||||
switch: { e: 6, n: 7, w: 8, s: 9 }
|
||||
- offset: [3, -0.25]
|
||||
switch: { w: 51, n: 52, e: 53, s: 54 }
|
||||
@@ -17,5 +17,11 @@
|
||||
<style lang="scss">
|
||||
p {
|
||||
margin-block: 0;
|
||||
|
||||
:global(kbd.icon) {
|
||||
display: inline-flex;
|
||||
font-size: inherit;
|
||||
translate: 0 0.2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
import("$lib/assets/layouts/m4g.yml").then(
|
||||
(it) => it.default as VisualLayout,
|
||||
),
|
||||
M4GR: () =>
|
||||
import("$lib/assets/layouts/m4gr.yml").then(
|
||||
(it) => it.default as VisualLayout,
|
||||
),
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -70,7 +74,7 @@
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: 96px;
|
||||
max-height: 20cm;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -12,7 +12,8 @@ import { browser } from "$app/environment";
|
||||
|
||||
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
||||
["TWO S3", { usbProductId: 0x0056, usbVendorId: 0x2886 }],
|
||||
["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }], // TODO: remove this after everyone has migrated
|
||||
["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }],
|
||||
["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }],
|
||||
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
|
||||
["X", { usbProductId: 33163, usbVendorId: 12346 }],
|
||||
@@ -25,6 +26,7 @@ const KEY_COUNTS = {
|
||||
LITE: 67,
|
||||
X: 256,
|
||||
M4G: 90,
|
||||
M4GR: 90,
|
||||
} as const;
|
||||
|
||||
if (
|
||||
@@ -35,6 +37,13 @@ if (
|
||||
await import("./tauri-serial");
|
||||
}
|
||||
|
||||
if (browser && navigator.serial === undefined && navigator.usb !== undefined) {
|
||||
// @ts-expect-error polyfill
|
||||
navigator.serial = await import("web-serial-polyfill").then(
|
||||
({ serial }) => serial,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getViablePorts(): Promise<SerialPort[]> {
|
||||
return navigator.serial.getPorts().then((ports) =>
|
||||
ports.filter((it) => {
|
||||
@@ -440,46 +449,94 @@ 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;
|
||||
});
|
||||
|
||||
if (result !== "OTA OK") {
|
||||
throw new Error(result);
|
||||
}
|
||||
|
||||
const writer2 = this.port.writable!.getWriter();
|
||||
try {
|
||||
await writer2.write(new TextEncoder().encode(`RST RESTART\r\n`));
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "input",
|
||||
value: "RST RESTART",
|
||||
});
|
||||
return it;
|
||||
});
|
||||
} finally {
|
||||
writer2.releaseLock();
|
||||
}
|
||||
|
||||
await this.suspend();
|
||||
} 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;
|
||||
@@ -29,7 +29,6 @@
|
||||
import { restoreFromFile } from "$lib/backup/backup";
|
||||
import { goto } from "$app/navigation";
|
||||
import { hotkeys } from "$lib/title";
|
||||
import { initMatrixClient } from "$lib/chat/chat";
|
||||
|
||||
const locale =
|
||||
((browser && localStorage.getItem("locale")) as Locales) || detectLocale();
|
||||
@@ -66,9 +65,6 @@
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
||||
await initSerial();
|
||||
}
|
||||
if (browser) {
|
||||
await initMatrixClient();
|
||||
}
|
||||
|
||||
if (data.importFile) {
|
||||
restoreFromFile(data.importFile);
|
||||
@@ -111,7 +107,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<!--{@html webManifestLink}-->
|
||||
{@html webManifestLink}
|
||||
<title>{$LL.TITLE()}</title>
|
||||
<meta name="description" content={$LL.DESCRIPTION()} />
|
||||
<meta name="theme-color" content={data.themeColor} />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { preference } from "$lib/preferences";
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import {
|
||||
createChordBackup,
|
||||
createLayoutBackup,
|
||||
createSettingsBackup,
|
||||
downloadBackup,
|
||||
downloadFile,
|
||||
restoreBackup,
|
||||
} from "$lib/backup/backup";
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
use:preference={"backup"}
|
||||
/>{$LL.backup.TITLE()}</label
|
||||
>
|
||||
</h2>
|
||||
<p class="disclaimer">
|
||||
<i>{$LL.backup.DISCLAIMER()}</i>
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>{$LL.backup.INDIVIDUAL()}</legend>
|
||||
<button onclick={() => downloadFile(createChordBackup())}>
|
||||
<span class="icon">piano</span>
|
||||
{$LL.configure.chords.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createLayoutBackup())}>
|
||||
<span class="icon">keyboard</span>
|
||||
{$LL.configure.layout.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createSettingsBackup())}>
|
||||
<span class="icon">settings</span>
|
||||
{$LL.configure.settings.TITLE()}
|
||||
</button>
|
||||
</fieldset>
|
||||
<div class="save">
|
||||
<button class="primary" onclick={downloadBackup}
|
||||
><span class="icon">download</span>{$LL.backup.DOWNLOAD()}</button
|
||||
>
|
||||
<label class="button"
|
||||
><input oninput={restoreBackup} type="file" /><span class="icon"
|
||||
>settings_backup_restore</span
|
||||
>{$LL.backup.RESTORE()}</label
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
h2 {
|
||||
margin-block-end: 0;
|
||||
|
||||
> label {
|
||||
gap: 10px;
|
||||
font-size: 24px;
|
||||
|
||||
> input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
margin-block: 16px;
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
max-width: 16cm;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.save {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,256 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { initSerial, serialPort } from "$lib/serial/connection";
|
||||
import { browser } from "$app/environment";
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import { preference } from "$lib/preferences";
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import { downloadBackup } from "$lib/backup/backup";
|
||||
|
||||
function reboot() {
|
||||
$serialPort?.reboot();
|
||||
$serialPort = undefined;
|
||||
powerDialog = false;
|
||||
setTimeout(() => {
|
||||
initSerial();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function bootloader() {
|
||||
downloadBackup();
|
||||
$serialPort?.bootloader();
|
||||
$serialPort = undefined;
|
||||
rebootInfo = true;
|
||||
powerDialog = false;
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
await initSerial(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert(
|
||||
"Connection failed. Is your device maybe pre-CCOS? Refer to the doc link in the bottom left for more information on your device.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let rebootInfo = $derived($serialPort !== undefined);
|
||||
let terminal = $state(false);
|
||||
let powerDialog = $state(false);
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<h2>{$LL.deviceManager.TITLE()}</h2>
|
||||
<label
|
||||
>{$LL.deviceManager.AUTO_CONNECT()}<input
|
||||
type="checkbox"
|
||||
use:preference={"autoConnect"}
|
||||
/></label
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if $serialPort}
|
||||
<p transition:slide>
|
||||
{$serialPort.company}
|
||||
{$serialPort.device}
|
||||
{$serialPort.chipset}
|
||||
<br />
|
||||
Version {$serialPort.version}
|
||||
</p>
|
||||
{#if $serialPort.version.toString() !== import.meta.env.VITE_LATEST_FIRMWARE}
|
||||
<a
|
||||
href="https://docs.charachorder.com/CharaChorder%20One.html#updating-the-firmware"
|
||||
>Firmware Update Instructions</a
|
||||
>
|
||||
{/if}
|
||||
<!--<button on:click={updateFirmware}>Update</button>-->
|
||||
{/if}
|
||||
|
||||
{#if browser}
|
||||
{#if navigator.userAgent.includes("Linux") && !$serialPort}
|
||||
<div class="linux-info">
|
||||
<p>{@html $LL.deviceManager.LINUX_PERMISSIONS()}</p>
|
||||
<p>
|
||||
In most cases you can simply follow the <a
|
||||
target="_blank"
|
||||
href="https://docs.arduino.cc/software/ide-v1/tutorials/Linux#please-read"
|
||||
>Arduino Guide</a
|
||||
> on serial port permissions.
|
||||
</p>
|
||||
<p>Special systems:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://wiki.archlinux.org/title/Arduino#Accessing_serial"
|
||||
>Arch and Arch-based like Manjaro or EndeavourOS</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://gist.github.com/CMCDragonkai/d00201ec143c9f749fc49533034e5009?permalink_comment_id=4670311#gistcomment-4670311"
|
||||
>NixOS</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://wiki.gentoo.org/wiki/Arduino#Grant_access_to_non-root_users"
|
||||
>Gentoo</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if rebootInfo}
|
||||
<p transition:slide>
|
||||
<b>{$LL.deviceManager.bootMenu.POWER_WARNING()}</b>
|
||||
</p>
|
||||
{/if}
|
||||
<div class="row">
|
||||
{#if $serialPort}
|
||||
<button
|
||||
class="secondary"
|
||||
onclick={() => {
|
||||
$serialPort?.forget();
|
||||
$serialPort = undefined;
|
||||
}}
|
||||
><span class="icon">usb_off</span
|
||||
>{$LL.deviceManager.DISCONNECT()}</button
|
||||
>
|
||||
{:else}
|
||||
<button class="error" onclick={connect}
|
||||
><span class="icon">usb</span>{$LL.deviceManager.CONNECT()}</button
|
||||
>
|
||||
{/if}
|
||||
<div class="row" style="justify-content: flex-end">
|
||||
<a
|
||||
href="/terminal"
|
||||
title={$LL.deviceManager.TERMINAL()}
|
||||
class="icon"
|
||||
class:disabled={$serialPort === undefined}
|
||||
onclick={() => (terminal = !terminal)}>terminal</a
|
||||
>
|
||||
<button
|
||||
class="icon"
|
||||
title={$LL.deviceManager.bootMenu.TITLE()}
|
||||
disabled={$serialPort === undefined}
|
||||
onclick={() => (powerDialog = !powerDialog)}>settings_power</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{#if powerDialog}
|
||||
<div
|
||||
class="backdrop"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
transition:fade={{ duration: 250 }}
|
||||
onclick={() => (powerDialog = !powerDialog)}
|
||||
onkeypress={(event) => {
|
||||
if (event.key === "Enter") powerDialog = !powerDialog;
|
||||
}}
|
||||
></div>
|
||||
<dialog open transition:slide={{ duration: 250 }}>
|
||||
<h3>{$LL.deviceManager.bootMenu.TITLE()}</h3>
|
||||
<button onclick={reboot}
|
||||
><span class="icon">restart_alt</span
|
||||
>{$LL.deviceManager.bootMenu.REBOOT()}</button
|
||||
>
|
||||
<button onclick={bootloader}
|
||||
><span class="icon">rule_settings</span
|
||||
>{$LL.deviceManager.bootMenu.BOOTLOADER()}</button
|
||||
>
|
||||
</dialog>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
h2 {
|
||||
margin-block: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block: 8px;
|
||||
}
|
||||
|
||||
.linux-info a {
|
||||
display: inline;
|
||||
padding-inline: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
|
||||
background: #0005;
|
||||
border-radius: 40px;
|
||||
}
|
||||
|
||||
dialog {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
margin-block-start: 16px;
|
||||
padding: 0;
|
||||
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
dialog > * {
|
||||
margin-inline: 16px;
|
||||
}
|
||||
|
||||
dialog > :first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-block: 8px;
|
||||
|
||||
color: var(--md-sys-color-on-secondary);
|
||||
|
||||
background: var(--md-sys-color-secondary);
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
}
|
||||
</style>
|
||||
@@ -8,11 +8,25 @@
|
||||
import { loadLocaleAsync } from "$i18n/i18n-util.async";
|
||||
import { tick } from "svelte";
|
||||
import SyncOverlay from "./SyncOverlay.svelte";
|
||||
import { serialPort } from "$lib/serial/connection";
|
||||
import {
|
||||
initSerial,
|
||||
serialPort,
|
||||
sync,
|
||||
syncProgress,
|
||||
syncStatus,
|
||||
} from "$lib/serial/connection";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
|
||||
let locale = $state(
|
||||
(browser && (localStorage.getItem("locale") as Locales)) || detectLocale(),
|
||||
);
|
||||
|
||||
let currentDevice = $derived(
|
||||
$serialPort
|
||||
? `${$serialPort.device.toLowerCase()}_${$serialPort.chipset.toLowerCase()}`
|
||||
: undefined,
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
localStorage.setItem("locale", locale);
|
||||
@@ -33,6 +47,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
await initSerial(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert(
|
||||
"Connection failed. Is your device maybe pre-CCOS? Refer to the doc link in the bottom left for more information on your device.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect(event: MouseEvent) {
|
||||
if (event.shiftKey) {
|
||||
sync();
|
||||
} else {
|
||||
$serialPort?.forget();
|
||||
$serialPort = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let languageSelect: HTMLSelectElement;
|
||||
</script>
|
||||
|
||||
@@ -40,39 +74,58 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
use:action={{ title: "Branch" }}
|
||||
href={import.meta.env.VITE_HOMEPAGE_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"><span class="icon">commit</span> v{version}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">bug_report</span> Issues</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={import.meta.env.VITE_DOCS_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">description</span> Docs</a
|
||||
<a
|
||||
href="/ccos/{currentDevice ? `${currentDevice}/` : ''}"
|
||||
use:action={{ title: "Updates" }}
|
||||
>
|
||||
CCOS {$serialPort?.version ?? "Updates"}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<div class="sync-box">
|
||||
{#if !$serialPort}
|
||||
<div class="warning">
|
||||
<span class="icon">warning</span>{$LL.deviceManager.NO_DEVICE()}
|
||||
</div>
|
||||
<button class="warning" onclick={connect} transition:slide={{ axis: "x" }}
|
||||
><span class="icon">usb</span>{$LL.deviceManager.CONNECT()}</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
transition:slide={{ axis: "x" }}
|
||||
onclick={disconnect}
|
||||
use:action={{
|
||||
title: "Disconnect<br><kbd class='icon'>shift</kbd> Sync",
|
||||
}}
|
||||
><b
|
||||
>{$serialPort.company}
|
||||
{$serialPort.device}
|
||||
{$serialPort.chipset}</b
|
||||
><span class="icon">usb_off</span></button
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if $syncStatus !== "done"}
|
||||
<progress
|
||||
transition:fade
|
||||
max={$syncProgress?.max ?? 1}
|
||||
value={$syncProgress?.current ?? 1}
|
||||
></progress>
|
||||
{/if}
|
||||
<SyncOverlay />
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={import.meta.env.VITE_STORE_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">shopping_bag</span> Store</a
|
||||
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">bug_report</span> Bugs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={import.meta.env.VITE_LEARN_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">school</span> Train</a
|
||||
<a href={import.meta.env.VITE_STORE_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">shopping_bag</span> Store</a
|
||||
>
|
||||
</li>
|
||||
<li class="hide-forced-colors">
|
||||
@@ -101,7 +154,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
<li>
|
||||
<!--<li>
|
||||
<div
|
||||
role="button"
|
||||
class="icon"
|
||||
@@ -116,7 +169,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</li>
|
||||
</li>-->
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
@@ -126,6 +179,37 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sync-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
progress {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
bottom: 0;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 32px);
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background: var(--md-sys-color-background);
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--md-sys-color-error);
|
||||
gap: 8px;
|
||||
|
||||
@@ -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,36 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { LL } from "$i18n/i18n-svelte";
|
||||
import { popup } from "$lib/popup";
|
||||
import { userPreferences } from "$lib/preferences";
|
||||
import { serialPort, syncStatus } from "$lib/serial/connection";
|
||||
import { action } from "$lib/title";
|
||||
import BackupPopup from "./BackupPopup.svelte";
|
||||
import ConnectionPopup from "./ConnectionPopup.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
onMount(async () => {
|
||||
if (browser && !$userPreferences.autoConnect) {
|
||||
connectButton.click();
|
||||
}
|
||||
});
|
||||
import { page } from "$app/stores";
|
||||
|
||||
const routes = [
|
||||
[
|
||||
{ href: "/config/chords/", icon: "dictionary", title: "Chords" },
|
||||
{
|
||||
href: "/config/settings/",
|
||||
icon: "cable",
|
||||
title: "Device",
|
||||
primary: true,
|
||||
},
|
||||
{ href: "/config/chords/", icon: "dictionary", title: "Library" },
|
||||
{ href: "/config/layout/", icon: "keyboard", title: "Layout" },
|
||||
{ href: "/config/settings/", icon: "tune", title: "Config" },
|
||||
],
|
||||
[
|
||||
{ href: "/learn", icon: "school", title: "Learn", wip: true },
|
||||
{ href: "/learn", icon: "description", title: "Docs" },
|
||||
// { href: "/learn", icon: "school", title: "Learn", wip: true },
|
||||
{
|
||||
href: import.meta.env.VITE_LEARN_URL,
|
||||
icon: "school",
|
||||
title: "Learn",
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
href: import.meta.env.VITE_DOCS_URL,
|
||||
icon: "description",
|
||||
title: "Docs",
|
||||
external: true,
|
||||
},
|
||||
{ href: "/editor", icon: "edit_document", title: "Editor", wip: true },
|
||||
{ href: "https://chat.dev.charachorder.io", icon: "chat", title: "Chat", wip: true },
|
||||
],
|
||||
[
|
||||
{ href: "/chat", icon: "chat", title: "Chat", wip: true },
|
||||
/*[
|
||||
{ href: "/plugin", icon: "code", title: "Plugin", wip: true },
|
||||
],
|
||||
] satisfies { href: string; icon: string; title: string; wip?: boolean }[][];
|
||||
],*/
|
||||
] satisfies {
|
||||
href: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
wip?: boolean;
|
||||
external?: boolean;
|
||||
primary?: boolean;
|
||||
}[][];
|
||||
|
||||
let connectButton: HTMLButtonElement;
|
||||
</script>
|
||||
@@ -39,10 +48,18 @@
|
||||
<nav>
|
||||
{#each routes as group}
|
||||
<ul>
|
||||
{#each group as { href, icon, title, wip }}
|
||||
{#each group as { href, icon, title, wip, external }}
|
||||
<li>
|
||||
<a class:wip {href}>
|
||||
<div class="icon">{icon}</div>
|
||||
<a
|
||||
class:wip
|
||||
{href}
|
||||
rel={external ? "noreferrer" : undefined}
|
||||
target={external ? "_blank" : undefined}
|
||||
class:active={$page.url.pathname.startsWith(href)}
|
||||
>
|
||||
<div class="icon">
|
||||
{icon}
|
||||
</div>
|
||||
<div class="content">
|
||||
{title}
|
||||
</div>
|
||||
@@ -52,28 +69,6 @@
|
||||
</ul>
|
||||
{/each}
|
||||
</nav>
|
||||
<ul class="sidebar-footer">
|
||||
<li>
|
||||
<button
|
||||
bind:this={connectButton}
|
||||
use:action={{ title: $LL.deviceManager.TITLE() }}
|
||||
use:popup={ConnectionPopup}
|
||||
class="icon connect"
|
||||
class:error={$serialPort === undefined}
|
||||
>
|
||||
cable
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
use:action={{ title: $LL.backup.TITLE() }}
|
||||
use:popup={BackupPopup}
|
||||
class="icon {$syncStatus}"
|
||||
>
|
||||
account_circle
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -109,12 +104,30 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
translate: 0 -8px;
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
&.active {
|
||||
> .content {
|
||||
translate: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
background: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<h1><a href="/ota-update/">Firmware Update</a></h1>
|
||||
<h1><a href="/ccos">Firmware Updates</a></h1>
|
||||
|
||||
{@render children()}
|
||||
|
||||
@@ -11,6 +11,5 @@
|
||||
margin-block: 1em;
|
||||
padding: 0;
|
||||
font-size: 3em;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
0
src/routes/(app)/ccos/OTA.svelte
Normal file
0
src/routes/(app)/ccos/OTA.svelte
Normal file
505
src/routes/(app)/ccos/[device]/[version]/+page.svelte
Normal file
505
src/routes/(app)/ccos/[device]/[version]/+page.svelte
Normal file
@@ -0,0 +1,505 @@
|
||||
<script lang="ts">
|
||||
import { downloadBackup } from "$lib/backup/backup";
|
||||
import { initSerial, serialPort } from "$lib/serial/connection";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import type { LoaderOptions, ESPLoader } from "esptool-js";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let working = $state(false);
|
||||
let success = $state(false);
|
||||
let error = $state<Error | undefined>(undefined);
|
||||
|
||||
let terminalOutput = $state("");
|
||||
|
||||
let step = $state(0);
|
||||
let eraseAll = $state(false);
|
||||
|
||||
let espLoader;
|
||||
|
||||
async function update() {
|
||||
working = true;
|
||||
error = undefined;
|
||||
success = false;
|
||||
const port = $serialPort!;
|
||||
$serialPort = undefined;
|
||||
try {
|
||||
const file = await fetch(
|
||||
`${data.meta.path}/${data.meta.update.ota!}`,
|
||||
).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.meta.target : 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`;
|
||||
}
|
||||
}
|
||||
|
||||
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 (!data.meta.update.uf2) return;
|
||||
const uf2Promise = fetch(
|
||||
`${data.meta.path}/${data.meta.update.uf2.name}`,
|
||||
).then((it) => it.blob());
|
||||
const handle = await window.showSaveFilePicker({
|
||||
id: `${data.meta.target}-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;
|
||||
}
|
||||
|
||||
async function espBootloader() {
|
||||
$serialPort?.forget();
|
||||
const port = await navigator.serial.requestPort();
|
||||
port.open({ baudRate: 1200 });
|
||||
}
|
||||
|
||||
async function connectEsp(port: SerialPort): Promise<ESPLoader> {
|
||||
const esptool = data.meta.update.esptool!;
|
||||
const { Transport, ESPLoader } = await import("esptool-js");
|
||||
const espLoader = new ESPLoader({
|
||||
transport: new Transport(port),
|
||||
baudrate: 9600, // Number(esptool.baud),
|
||||
romBaudrate: 9600, // Number(esptool.baud),
|
||||
debugLogging: true,
|
||||
terminal: {
|
||||
clean: () => {
|
||||
terminalOutput = "";
|
||||
},
|
||||
writeLine: (data) => {
|
||||
terminalOutput += data + "\n";
|
||||
},
|
||||
write: (data) => {
|
||||
terminalOutput += data;
|
||||
},
|
||||
},
|
||||
} satisfies LoaderOptions);
|
||||
await espLoader.detectChip(esptool.before);
|
||||
if (!espLoader.IS_STUB) {
|
||||
await espLoader.runStub();
|
||||
}
|
||||
|
||||
return espLoader;
|
||||
}
|
||||
|
||||
async function flashImages() {
|
||||
const port = await navigator.serial.requestPort();
|
||||
try {
|
||||
const esptool = data.meta.update.esptool!;
|
||||
espLoader = await connectEsp(port);
|
||||
const fileArray = await Promise.all(
|
||||
Object.entries(esptool.files).map(([offset, name]) =>
|
||||
fetch(`${data.meta.path}/${name}`)
|
||||
.then((it) => it.blob())
|
||||
.then((it) => it.text())
|
||||
.then((it) => ({
|
||||
address: Number(offset),
|
||||
data: it,
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
await espLoader.writeFlash({
|
||||
flashSize: esptool.flash_size,
|
||||
flashMode: esptool.flash_mode,
|
||||
flashFreq: esptool.flash_freq,
|
||||
compress: true,
|
||||
eraseAll,
|
||||
fileArray,
|
||||
});
|
||||
} finally {
|
||||
port.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function eraseSPI() {
|
||||
const port = await navigator.serial.requestPort();
|
||||
try {
|
||||
console.log(data.meta);
|
||||
const spiFlash = data.meta.spi_flash!;
|
||||
espLoader = await connectEsp(port);
|
||||
|
||||
/*espLoader.flashSpiAttach(
|
||||
(spiFlash.connection.clk << 0) |
|
||||
(spiFlash.connection.q << 8) |
|
||||
(spiFlash.connection.d << 16) |
|
||||
(spiFlash.connection.cs << 24),
|
||||
);
|
||||
espLoader.flashId();*/
|
||||
} finally {
|
||||
port.close();
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<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
|
||||
<b
|
||||
>{$serialPort.company}
|
||||
{$serialPort.device}
|
||||
{$serialPort.chipset}</b
|
||||
>
|
||||
will be updated from <b class="version">{$serialPort.version}</b> to
|
||||
<b class="version">{data.meta.version}</b>
|
||||
</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>
|
||||
|
||||
<h3>Manual Update</h3>
|
||||
{/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>
|
||||
|
||||
<pre>{terminalOutput}</pre>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
h2 > em {
|
||||
font-style: normal;
|
||||
transition: color 200ms ease;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-block-start: 4em;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: calc(min(100%, 16cm));
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(120deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: rotate(120deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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[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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.inline-link {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.correct-device {
|
||||
color: var(--md-sys-color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.incorrect-device {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.esp-buttons {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
50
src/routes/(app)/ccos/[device]/[version]/+page.ts
Normal file
50
src/routes/(app)/ccos/[device]/[version]/+page.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import type { FileListing, Listing } from "../../listing";
|
||||
import type { VersionMeta } from "./meta";
|
||||
|
||||
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();
|
||||
const meta: VersionMeta | undefined = data.some(
|
||||
(entry) => entry.type === "file" && entry.name === "meta.json",
|
||||
)
|
||||
? await fetch(
|
||||
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/meta.json`,
|
||||
).then((res) => res.json())
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
meta: {
|
||||
version: meta?.version ?? params.version,
|
||||
target: meta?.target ?? params.device,
|
||||
path: `${import.meta.env.VITE_FIRMWARE_URL}${params.device}/${params.version}`,
|
||||
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("."),
|
||||
development_mode: meta?.development_mode ?? 0,
|
||||
update: {
|
||||
uf2:
|
||||
(data.find(
|
||||
(entry) =>
|
||||
entry.type === "file" &&
|
||||
entry.name === (meta?.update?.uf2 ?? "CURRENT.UF2"),
|
||||
) as FileListing) ?? undefined,
|
||||
ota:
|
||||
data.find(
|
||||
(entry) =>
|
||||
entry.type === "file" &&
|
||||
entry.name === (meta?.update?.ota ?? "firmware.bin"),
|
||||
) ?? undefined,
|
||||
esptool: meta?.update?.esptool ?? undefined,
|
||||
},
|
||||
files: data.filter(
|
||||
(entry) =>
|
||||
entry.type === "file" && (!meta?.files || entry.name in meta.files),
|
||||
) as FileListing[],
|
||||
spi_flash: meta?.spi_flash ?? undefined,
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
41
src/routes/(app)/ccos/[device]/[version]/meta.ts
Normal file
41
src/routes/(app)/ccos/[device]/[version]/meta.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface VersionMeta {
|
||||
version: string;
|
||||
target: string;
|
||||
git_commit: string;
|
||||
git_is_dirty: boolean;
|
||||
git_date: string;
|
||||
public_build: boolean;
|
||||
development_mode: number;
|
||||
update: {
|
||||
ota: string | null;
|
||||
uf2: string | null;
|
||||
esptool: EspToolData | null;
|
||||
};
|
||||
files: string[];
|
||||
spi_flash: SPIFlashInfo | null;
|
||||
}
|
||||
|
||||
export interface SPIFlashInfo {
|
||||
type: string;
|
||||
size: string;
|
||||
connection: SPIConnection;
|
||||
}
|
||||
|
||||
export interface SPIConnection {
|
||||
clk: number;
|
||||
q: number;
|
||||
d: number;
|
||||
hd: number;
|
||||
cs: number;
|
||||
}
|
||||
|
||||
export interface EspToolData {
|
||||
chip: string;
|
||||
baud: string;
|
||||
before: string;
|
||||
after: string;
|
||||
flash_mode: string;
|
||||
flash_freq: string;
|
||||
flash_size: string;
|
||||
files: Record<string, string>;
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { isLoggedIn, matrix } from "$lib/chat/chat";
|
||||
import { initMatrixClient, isLoggedIn, matrix } from "$lib/chat/chat";
|
||||
import { flip } from "svelte/animate";
|
||||
import { slide } from "svelte/transition";
|
||||
import Login from "./Login.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
onMount(async () => {
|
||||
if (browser) {
|
||||
await initMatrixClient();
|
||||
}
|
||||
});
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let { children }: { children?: Snippet } = $props();
|
||||
|
||||
let paths = $derived([
|
||||
{
|
||||
href: "/config/chords/",
|
||||
title: $LL.configure.chords.TITLE(),
|
||||
icon: "piano",
|
||||
},
|
||||
{
|
||||
href: "/config/layout/",
|
||||
title: $LL.configure.layout.TITLE(),
|
||||
icon: "keyboard",
|
||||
},
|
||||
{
|
||||
href: "/config/settings/",
|
||||
title: $LL.configure.settings.TITLE(),
|
||||
icon: "settings",
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
{#each paths as { href, title, icon }}
|
||||
<a {href} class:active={$page.url.pathname.startsWith(href)}>
|
||||
<span class="icon">{icon}</span>
|
||||
{title}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
padding: 8px;
|
||||
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
a.active {
|
||||
--icon-fill: 1;
|
||||
|
||||
color: var(--md-sys-color-on-primary);
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,8 @@
|
||||
import { canShare, triggerShare } from "$lib/share";
|
||||
import { action } from "$lib/title";
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import ConfigTabs from "./ConfigTabs.svelte";
|
||||
import EditActions from "./EditActions.svelte";
|
||||
import { sync, syncStatus } from "$lib/serial/connection";
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
@@ -12,8 +12,6 @@
|
||||
<EditActions />
|
||||
</div>
|
||||
|
||||
<ConfigTabs />
|
||||
|
||||
<div class="actions">
|
||||
{#if $canShare}
|
||||
<button
|
||||
@@ -40,7 +38,7 @@
|
||||
<style lang="scss">
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
width: calc(min(100%, 28cm));
|
||||
margin-block: 8px;
|
||||
@@ -48,6 +46,20 @@
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
@keyframes syncing {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.syncing {
|
||||
transform-origin: 50% 49%;
|
||||
animation: syncing 1s linear infinite;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
74
src/routes/(app)/config/PageTransition.svelte
Normal file
74
src/routes/(app)/config/PageTransition.svelte
Normal file
@@ -0,0 +1,74 @@
|
||||
<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 done: undefined | (() => void) = $state(undefined);
|
||||
let animationDone: Promise<void>;
|
||||
|
||||
let isNavigating = $state(false);
|
||||
|
||||
const routeOrder = [
|
||||
"/config/settings/",
|
||||
"/config/chords/",
|
||||
"/config/layout/",
|
||||
];
|
||||
|
||||
function outroEnd() {
|
||||
done?.();
|
||||
}
|
||||
|
||||
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) => {
|
||||
done = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
afterNavigate(async () => {
|
||||
await animationDone;
|
||||
isNavigating = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !isNavigating}
|
||||
<main
|
||||
in:fly={{
|
||||
y: inDirection * 24,
|
||||
duration: 150,
|
||||
delay: 1, // flicker for some reason without this
|
||||
easing: expoOut,
|
||||
}}
|
||||
out:fly={{ y: outDirection * 24, duration: 150, easing: expoIn }}
|
||||
onoutroend={outroEnd}
|
||||
>
|
||||
{@render children()}
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
main {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -43,7 +43,7 @@
|
||||
buildIndex($chords, $osLayout).then(searchIndex.set);
|
||||
});
|
||||
|
||||
function encodeChord(chord: ChordInfo, osLayout: Map<string, string>) {
|
||||
function encodeChord(chord: ChordInfo, osLayout: Map<string, string>, onlyPhrase: boolean = false) {
|
||||
const plainPhrase: string[] = [""];
|
||||
const extraActions: string[] = [];
|
||||
const extraCodes: string[] = [];
|
||||
@@ -103,6 +103,10 @@
|
||||
return result ?? `0x${it.toString(16)}`;
|
||||
});
|
||||
|
||||
if (onlyPhrase) {
|
||||
return plainPhrase.join();
|
||||
}
|
||||
|
||||
return [
|
||||
...plainPhrase,
|
||||
`+${input.join("+")}`,
|
||||
@@ -182,7 +186,7 @@
|
||||
function downloadVocabulary() {
|
||||
const vocabulary = new Set(
|
||||
$chords.map((it) =>
|
||||
"phrase" in it ? plainPhrase(it.phrase, $osLayout).trim() : "",
|
||||
"phrase" in it ? encodeChord(it, $osLayout, true).trim() : "",
|
||||
),
|
||||
);
|
||||
vocabulary.delete("");
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
id.splice(id.indexOf(0), 1);
|
||||
id.push(0);
|
||||
while ($chords.some((it) => JSON.stringify(it.id) === JSON.stringify(id))) {
|
||||
id[id.length - 1]!++;
|
||||
id[id.length - 1] = id[id.length - 1]! + 1;
|
||||
}
|
||||
|
||||
changes.update((changes) => {
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
import { serialPort } from "$lib/serial/connection";
|
||||
import { setting } from "$lib/setting";
|
||||
import ResetPopup from "./ResetPopup.svelte";
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import {
|
||||
createChordBackup,
|
||||
createLayoutBackup,
|
||||
createSettingsBackup,
|
||||
downloadBackup,
|
||||
downloadFile,
|
||||
restoreBackup,
|
||||
} from "$lib/backup/backup";
|
||||
import { preference } from "$lib/preferences";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -11,8 +21,67 @@
|
||||
<meta name="description" content="Change your device's settings" />
|
||||
</svelte:head>
|
||||
|
||||
{#if $serialPort}
|
||||
<section>
|
||||
<section>
|
||||
<fieldset>
|
||||
<legend>{$LL.backup.TITLE()}</legend>
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
use:preference={"backup"}
|
||||
/>{$LL.backup.AUTO_BACKUP()}</label
|
||||
>
|
||||
<p class="disclaimer">
|
||||
{$LL.backup.DISCLAIMER()}
|
||||
</p>
|
||||
<div class="row" style="margin-top: auto">
|
||||
<button onclick={() => downloadFile(createChordBackup())}>
|
||||
<span class="icon">piano</span>
|
||||
{$LL.configure.chords.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createLayoutBackup())}>
|
||||
<span class="icon">keyboard</span>
|
||||
{$LL.configure.layout.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createSettingsBackup())}>
|
||||
<span class="icon">settings</span>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="primary" onclick={downloadBackup}
|
||||
><span class="icon">download</span>{$LL.backup.DOWNLOAD()}</button
|
||||
>
|
||||
<label class="button"
|
||||
><input oninput={restoreBackup} type="file" /><span class="icon"
|
||||
>settings_backup_restore</span
|
||||
>{$LL.backup.RESTORE()}</label
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Device</legend>
|
||||
<label
|
||||
>{$LL.deviceManager.AUTO_CONNECT()}<input
|
||||
type="checkbox"
|
||||
use:preference={"autoConnect"}
|
||||
/></label
|
||||
>
|
||||
{#if $serialPort}
|
||||
<label
|
||||
>Boot message<input type="checkbox" use:setting={{ id: 0x93 }} /></label
|
||||
>
|
||||
<label
|
||||
>GTM Realtime Feedback<input
|
||||
type="checkbox"
|
||||
use:setting={{ id: 0x92 }}
|
||||
/></label
|
||||
>
|
||||
<button class="outline" use:popup={ResetPopup}>Reset...</button>
|
||||
{/if}
|
||||
</fieldset>
|
||||
|
||||
{#if $serialPort}
|
||||
<fieldset>
|
||||
<legend
|
||||
><label
|
||||
@@ -231,20 +300,6 @@
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Device</legend>
|
||||
<label
|
||||
>Boot message<input type="checkbox" use:setting={{ id: 0x93 }} /></label
|
||||
>
|
||||
<label
|
||||
>GTM Realtime Feedback<input
|
||||
type="checkbox"
|
||||
use:setting={{ id: 0x92 }}
|
||||
/></label
|
||||
>
|
||||
<button class="outline" use:popup={ResetPopup}>Reset...</button>
|
||||
</fieldset>
|
||||
|
||||
{#if $serialPort.device === "LITE"}
|
||||
<fieldset>
|
||||
<legend
|
||||
@@ -275,8 +330,8 @@
|
||||
</select>
|
||||
</fieldset>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
@@ -315,18 +370,21 @@
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
font-size: 12px;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: 400px;
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 24px;
|
||||
|
||||
&:has(> legend input:not(:checked)) > :not(legend) {
|
||||
/*&:has(> legend input:not(:checked)) > :not(legend) {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}*/
|
||||
|
||||
> label {
|
||||
position: relative;
|
||||
@@ -429,4 +487,14 @@
|
||||
content: "•";
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin-block: 8px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Action } from "svelte/action";
|
||||
import ConfirmChallenge from "./ConfirmChallenge.svelte";
|
||||
import tippy from "tippy.js";
|
||||
import { mount, unmount } from "svelte";
|
||||
|
||||
export const confirmChallenge: Action<
|
||||
HTMLElement,
|
||||
{ onConfirm: () => void; challenge: string }
|
||||
> = (node, { onConfirm, challenge }) => {
|
||||
let component: ConfirmChallenge | undefined;
|
||||
let component: {} | undefined;
|
||||
let target: HTMLElement | undefined;
|
||||
const edit = tippy(node, {
|
||||
interactive: true,
|
||||
@@ -15,15 +16,22 @@ export const confirmChallenge: Action<
|
||||
target = instance.popper.querySelector(".tippy-content") as HTMLElement;
|
||||
target.classList.add("active");
|
||||
if (component === undefined) {
|
||||
component = new ConfirmChallenge({ target, props: { challenge } });
|
||||
component.$on("confirm", () => {
|
||||
edit.hide();
|
||||
onConfirm();
|
||||
component = mount(ConfirmChallenge, {
|
||||
target,
|
||||
props: {
|
||||
challenge,
|
||||
onconfirm() {
|
||||
edit.hide();
|
||||
onConfirm();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
onHidden() {
|
||||
component?.$destroy();
|
||||
if (component) {
|
||||
unmount(component);
|
||||
}
|
||||
target?.classList.remove("active");
|
||||
component = undefined;
|
||||
},
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { serialPort } from "$lib/serial/connection";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
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}
|
||||
<p>OTA update</p>
|
||||
{: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>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
h2 > em {
|
||||
font-style: normal;
|
||||
transition: color 200ms ease;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -155,30 +155,31 @@
|
||||
doc: examplePlugin,
|
||||
});
|
||||
});
|
||||
let channels = $derived(
|
||||
$serialPort
|
||||
? ({
|
||||
getVersion: async (..._args: unknown[]) => $serialPort.version,
|
||||
getDevice: async (..._args: unknown[]) => $serialPort.device,
|
||||
commit: async (..._args: unknown[]) => {
|
||||
if (
|
||||
confirm(
|
||||
"Perform a commit? Settings are already applied until the next reboot.\n\n" +
|
||||
"Excessive commits can lead to premature breakdowns, as the settings storage is only rated for 10,000-25,000 commits.\n\n" +
|
||||
"Click OK to perform the commit anyways.",
|
||||
)
|
||||
) {
|
||||
return $serialPort.commit();
|
||||
}
|
||||
},
|
||||
...Object.fromEntries(
|
||||
charaMethods.map(
|
||||
(it) => [it, $serialPort[it].bind($serialPort)] as const,
|
||||
),
|
||||
),
|
||||
} satisfies Record<string, Function>)
|
||||
: ({} as any),
|
||||
);
|
||||
|
||||
let channels = $derived.by(() => {
|
||||
if (!$serialPort) return {} as any;
|
||||
return {
|
||||
getVersion: (..._args: unknown[]) => Promise.resolve($serialPort.version),
|
||||
getDevice: (..._args: unknown[]) => Promise.resolve($serialPort.device),
|
||||
commit: (..._args: unknown[]) => {
|
||||
if (
|
||||
confirm(
|
||||
"Perform a commit? Settings are already applied until the next reboot.\n\n" +
|
||||
"Excessive commits can lead to premature breakdowns, as the settings storage is only rated for 10,000-25,000 commits.\n\n" +
|
||||
"Click OK to perform the commit anyways.",
|
||||
)
|
||||
) {
|
||||
return Promise.resolve($serialPort.commit());
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
...Object.fromEntries(
|
||||
charaMethods.map(
|
||||
(it) => [it, $serialPort[it].bind($serialPort)] as const,
|
||||
),
|
||||
),
|
||||
} satisfies Record<string, Function>;
|
||||
});
|
||||
|
||||
async function onMessage(event: MessageEvent) {
|
||||
if (event.origin !== "null" || event.source !== frame.contentWindow) return;
|
||||
|
||||
@@ -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";
|
||||
@@ -58,6 +58,7 @@ export default defineConfig({
|
||||
"client/**/*.{js,css,ico,woff2,csv,png,webp,svg,webmanifest}",
|
||||
"prerendered/**/*.html",
|
||||
],
|
||||
globIgnores: ["prerendered/pages/ccos/**/*"],
|
||||
ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/],
|
||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user