From fc86b313379fbc1dd4947d8461fce7c4c2113065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Fri, 27 Oct 2023 19:39:26 +0200 Subject: [PATCH] feat: chord editing prototype feat: lazy device connections feat: backup docs feat: chord library pagination --- docs/BACKUP.md | 64 ++++++++++++ icons.config.ts | 2 + src/i18n/de/index.ts | 8 ++ src/i18n/en/index.ts | 8 ++ src/lib/components/ActionStringEdit.svelte | 113 +++++++++++++++++++++ src/lib/components/Tooltip.svelte | 2 +- src/lib/serial/connection.ts | 2 +- src/lib/serial/device.ts | 42 ++++---- src/lib/serial/keymap-codes.ts | 14 +++ src/lib/share/chara-file.ts | 6 +- src/lib/style/form/_button.scss | 6 ++ src/routes/EditActions.svelte | 10 +- src/routes/config/chords/+page.svelte | 89 +++++++++------- 13 files changed, 306 insertions(+), 60 deletions(-) create mode 100644 docs/BACKUP.md create mode 100644 src/lib/components/ActionStringEdit.svelte diff --git a/docs/BACKUP.md b/docs/BACKUP.md new file mode 100644 index 00000000..4f5c8857 --- /dev/null +++ b/docs/BACKUP.md @@ -0,0 +1,64 @@ +# Chara Backup Format, Version 1 + +JSON Schema files: TBD + +Chara backups are serialized using JSON, in this general format: + +```json +{ + "charaVersion": 1, + "type": "..." +} +``` + +The presence of the key `charaVersion` uniquely identifies the JSON file as a chara backup file and serves +as a discriminator against other generic JSON files. This key is mandatory for that reason. + +## Type `layout` + +```json +{ + "charaVersion": 1, + "type": "layout", + "device": "one", + "layers": [[], [], []] +} +``` + +Devices at the current point in time may be identified as either `lite` or `one`, more to come in the future. + +Layers are serialized as an array of `[layer1, layer2, layer3]` in the internal order of the key, each specifying +an action code. Action codes of `0` are considered unassigned. + +## Type `chords` + +```json +{ + "charaVersion": 1, + "type": "chords", + "chords": [ + [ + [1, 2, 3], + [3, 4, 5] + ], + [ + [6, 7, 8], + [9, 10, 11] + ] + ] +} +``` + +Chords are serialized using a key-value mapping of chord action codes to actions. + +## Type `settings` + +```json +{ + "charaVersion": 1, + "type": "settings", + "settings": [0, 1, 3, 6] +} +``` + +Settings are serialized as an array of the values in the way they appear on the device. diff --git a/icons.config.ts b/icons.config.ts index 0a3d8154..d9f2c21d 100644 --- a/icons.config.ts +++ b/icons.config.ts @@ -67,6 +67,8 @@ const config: IconsConfig = { "bolt", "undo", "redo", + "navigate_before", + "navigate_next", ], codePoints: { speed: "e9e4", diff --git a/src/i18n/de/index.ts b/src/i18n/de/index.ts index 9bcd411d..952b2a11 100644 --- a/src/i18n/de/index.ts +++ b/src/i18n/de/index.ts @@ -62,6 +62,14 @@ const de = { INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.", DOWNLOAD_APP: "Desktop-app herunterladen", }, + changes: { + TITLE: "Änderungen anwenden", + CHORD_ADD: "{0} Akkord{{|e}} hinzugefügt", + CHORD_EDIT: "{0} Akkord{{|e}} bearbeitet", + CHORD_DELETE: "{0} Akkord{{|e}} entfernt", + SETTING_CHANGE: "{0} Einstellung{{|en}} geändert", + LAYOUT_CHANGE: "{0} Layout-belegung{{|en}} geändert", + }, configure: { chords: { TITLE: "Akkorde", diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index bbc2738a..052cc552 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -60,6 +60,14 @@ const en = { INFO_BROWSER_SUFFIX: ".", DOWNLOAD_APP: "Download the desktop app", }, + changes: { + TITLE: "Apply changes", + CHORD_ADD: "{0} chord{{|s}} added", + CHORD_EDIT: "{0} chord{{|s}} edited", + CHORD_DELETE: "{0} chord{{|s}} deleted", + SETTING_CHANGE: "{0} setting{{|s}} changed", + LAYOUT_CHANGE: "{0} layout key{{|s}} changed", + }, configure: { chords: { TITLE: "Chords", diff --git a/src/lib/components/ActionStringEdit.svelte b/src/lib/components/ActionStringEdit.svelte new file mode 100644 index 00000000..ba61d0d1 --- /dev/null +++ b/src/lib/components/ActionStringEdit.svelte @@ -0,0 +1,113 @@ + + + + + + {#each actions as char} + {@const action = KEYMAP_CODES[char]} + {#if action?.id && /^\w$/.test(action.id)} + {KEYMAP_CODES[char].id} + {:else if action} + {action.icon || action.id} + {:else} + {action} + {/if} + {/each} + + + + diff --git a/src/lib/components/Tooltip.svelte b/src/lib/components/Tooltip.svelte index b8153143..ca126d30 100644 --- a/src/lib/components/Tooltip.svelte +++ b/src/lib/components/Tooltip.svelte @@ -4,7 +4,7 @@ {#if title} -

{title}

+

{@html title}

{/if} {#if shortcut} diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts index 97860cf0..eda23baa 100644 --- a/src/lib/serial/connection.ts +++ b/src/lib/serial/connection.ts @@ -25,7 +25,7 @@ export const layout = persistentWritable( export interface Change { layout?: Record> - chords?: never + chords?: Array> settings?: Record } diff --git a/src/lib/serial/device.ts b/src/lib/serial/device.ts index c2451f73..03dba120 100644 --- a/src/lib/serial/device.ts +++ b/src/lib/serial/device.ts @@ -42,6 +42,7 @@ export class CharaDevice { !manual && ports.length === 1 ? ports[0] : await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]}) + await this.port.open({baudRate: this.baudRate}) const info = this.port.getInfo() serialLog.update(it => { @@ -53,7 +54,27 @@ export class CharaDevice { }) return it }) + await this.port.close() + const [version] = await this.send("VERSION") + this.version = version.split(".").map(Number) as [number, number, number] + const [company, device, chipset] = await this.send("ID") + this.company = company as "CHARACHORDER" + this.device = device as "ONE" | "LITE" + this.chipset = chipset as "M0" | "S2" + } + + private async suspend() { + await this.reader.cancel() + await this.streamClosed.catch(() => { + /** noop */ + }) + this.reader.releaseLock() + await this.port.close() + } + + private async wake() { + await this.port.open({baudRate: this.baudRate}) const decoderStream = new TextDecoderStream() this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, { signal: this.abortController1.signal, @@ -64,13 +85,6 @@ export class CharaDevice { signal: this.abortController2.signal, }) .getReader() - - const [version] = await this.send("VERSION") - this.version = version.split(".").map(Number) as [number, number, number] - const [company, device, chipset] = await this.send("ID") - this.company = company as "CHARACHORDER" - this.device = device as "ONE" | "LITE" - this.chipset = chipset as "M0" | "S2" } private async internalRead() { @@ -105,19 +119,9 @@ export class CharaDevice { } async forget() { - await this.disconnect() await this.port.forget() } - async disconnect() { - await this.reader.cancel() - await this.streamClosed.catch(() => { - /** noop */ - }) - this.reader.releaseLock() - await this.port.close() - } - /** * Read/write to serial port */ @@ -132,8 +136,10 @@ export class CharaDevice { const exec = new Promise(async resolve => { let result!: T try { + await this.wake() result = await callback(send, read) } finally { + await this.suspend() this.lock = undefined resolve(result) } @@ -252,7 +258,6 @@ export class CharaDevice { */ async reboot() { await this.send("RST") - await this.disconnect() // TODO: reconnect } @@ -261,7 +266,6 @@ export class CharaDevice { */ async bootloader() { await this.send("RST BOOTLOADER") - await this.disconnect() // TODO: more... } diff --git a/src/lib/serial/keymap-codes.ts b/src/lib/serial/keymap-codes.ts index d56f9940..ec32a91b 100644 --- a/src/lib/serial/keymap-codes.ts +++ b/src/lib/serial/keymap-codes.ts @@ -19,3 +19,17 @@ export const KEYMAP_CODES: Record = Object.fromEntries( ]), ), ) + +export const KEYMAP_IDS: Map = new Map( + keymaps + .flatMap(category => + Object.entries(category.actions).map( + ([code, action]) => [action.id!, {...action, code: Number(code), category}] as const, + ), + ) + .filter(([id]) => id !== undefined), +) + +export const specialKeycodes = new Map([ + [" ", 32], // Space +]) diff --git a/src/lib/share/chara-file.ts b/src/lib/share/chara-file.ts index 43b6c858..aed79eb3 100644 --- a/src/lib/share/chara-file.ts +++ b/src/lib/share/chara-file.ts @@ -9,7 +9,11 @@ export interface CharaLayoutFile extends CharaFile<"layout"> { } export interface CharaChordFile extends CharaFile<"chords"> { - chords: [number[], number[]] + chords: [number[], number[]][] +} + +export interface CharaChordSettings extends CharaFile<"settings"> { + settings: number[] } export type CharaFiles = CharaLayoutFile | CharaChordFile diff --git a/src/lib/style/form/_button.scss b/src/lib/style/form/_button.scss index 88069056..62e83103 100644 --- a/src/lib/style/form/_button.scss +++ b/src/lib/style/form/_button.scss @@ -28,6 +28,8 @@ button { transition: all 250ms ease; &.icon { + display: inline-flex; + aspect-ratio: 1; padding-block: 0; padding-inline: 0; @@ -41,6 +43,10 @@ button { color: var(--md-sys-color-on-primary); background: var(--md-sys-color-primary); } + + &.compact { + height: 32px; + } } label:has(input):hover, diff --git a/src/routes/EditActions.svelte b/src/routes/EditActions.svelte index 2ff7c5be..d1fbc903 100644 --- a/src/routes/EditActions.svelte +++ b/src/routes/EditActions.svelte @@ -40,8 +40,14 @@
{#if $changes.length !== 0} - bolt{$LL.saveActions.APPLY()} {/if} diff --git a/src/routes/config/chords/+page.svelte b/src/routes/config/chords/+page.svelte index 84a5b117..bd3da764 100644 --- a/src/routes/config/chords/+page.svelte +++ b/src/routes/config/chords/+page.svelte @@ -1,9 +1,29 @@ @@ -35,35 +58,32 @@ placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)} on:input={search} /> + {page + 1} / {lastPage + 1} + +
- - -
+
- {#each items.slice(0, 50) as [{ phrase, actions }, i]} - + {#each items.slice(page * pageSize, (page + 1) * pageSize) as [chord]} + + {/each} @@ -98,16 +118,11 @@ } section { - --scrollbar-color: var(--md-sys-color-surface-variant); - - scrollbar-gutter: stable; - position: relative; - overflow-x: hidden; - overflow-y: auto; - display: flex; + overflow: hidden; + height: 100%; padding-inline: 8px; border-radius: 16px; @@ -143,10 +158,12 @@ text-align: start; } - td { - display: flex; - gap: 4px; - align-items: stretch; - justify-content: flex-end; + .table-buttons { + opacity: 0; + transition: opacity 75ms ease; + } + + tr:hover > .table-buttons { + opacity: 1; }
- {#each phrase as char} - {KEYMAP_CODES[char].id} - {/each} + - {#each actions as action} - {@const keyInfo = KEYMAP_CODES[action]} - {#if keyInfo} - {keyInfo.icon || keyInfo.id} - {:else} -
{action}
- {/if} - {/each} + +
+ +