mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-19 08:22:53 +00:00
feat: chord editing prototype
feat: lazy device connections feat: backup docs feat: chord library pagination
This commit is contained in:
113
src/lib/components/ActionStringEdit.svelte
Normal file
113
src/lib/components/ActionStringEdit.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES, KEYMAP_IDS, specialKeycodes} from "$lib/serial/keymap-codes"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import type ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||
|
||||
export let actions: number[]
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("selectionchange", select)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener("selectionchange", select)
|
||||
})
|
||||
|
||||
function input(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight":
|
||||
case "Escape":
|
||||
case "Tab": {
|
||||
return
|
||||
}
|
||||
case "Backspace": {
|
||||
caretPosition--
|
||||
if (caretPosition >= 0) actions = actions.toSpliced(caretPosition, 1)
|
||||
else caretPosition = 0
|
||||
break
|
||||
}
|
||||
case "Delete": {
|
||||
if (caretPosition < actions.length) actions = actions.toSpliced(caretPosition, 1)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (specialKeycodes.has(event.key)) {
|
||||
actions = actions.toSpliced(caretPosition, 0, 32)
|
||||
} else if (KEYMAP_IDS.has(event.key)) {
|
||||
actions = actions.toSpliced(caretPosition, 0, KEYMAP_IDS.get(event.key)!.code)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
event.preventDefault()
|
||||
console.log(event.key)
|
||||
}
|
||||
|
||||
function select() {
|
||||
const selection = document.getSelection()
|
||||
if (!selection || !selection.containsNode(field, true)) return
|
||||
|
||||
let node = selection.anchorNode?.parentNode
|
||||
let i = 0
|
||||
while (node) {
|
||||
i++
|
||||
node = node.previousSibling
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
const clonedRange = range.cloneRange()
|
||||
clonedRange.selectNodeContents(field)
|
||||
clonedRange.setEnd(range.endContainer, range.endOffset)
|
||||
|
||||
caretPosition = (i - 1) / 2 + clonedRange.endOffset
|
||||
console.log(caretPosition)
|
||||
}
|
||||
|
||||
let editDialog: ActionSelector
|
||||
let caretPosition: number
|
||||
let field: HTMLSpanElement
|
||||
</script>
|
||||
|
||||
<svelte:window on:selectionchange={select} />
|
||||
|
||||
<span
|
||||
bind:this={field}
|
||||
contenteditable
|
||||
on:keydown={input}
|
||||
spellcheck="false"
|
||||
on:select|preventDefault={select}
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
>
|
||||
{#each actions as char}
|
||||
{@const action = KEYMAP_CODES[char]}
|
||||
{#if action?.id && /^\w$/.test(action.id)}
|
||||
<span data-action={char}>{KEYMAP_CODES[char].id}</span>
|
||||
{:else if action}
|
||||
<kbd data-action={char} title={action.title} class:icon={!!action.icon}>{action.icon || action.id}</kbd>
|
||||
{:else}
|
||||
<kbd data-action={char}>{action}</kbd>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- <kbd class="icon" style="background: red">abc</kbd> -->
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
kbd {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
:not(kbd) + kbd {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
span[contenteditable]:focus-within {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
</script>
|
||||
|
||||
{#if title}
|
||||
<p>{title}</p>
|
||||
<p>{@html title}</p>
|
||||
{/if}
|
||||
|
||||
{#if shortcut}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const layout = persistentWritable<CharaLayout>(
|
||||
|
||||
export interface Change {
|
||||
layout?: Record<number, Record<number, number>>
|
||||
chords?: never
|
||||
chords?: Array<Record<"delete" | "edit" | "add", Chord>>
|
||||
settings?: Record<number, number>
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T>(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...
|
||||
}
|
||||
|
||||
|
||||
@@ -19,3 +19,17 @@ export const KEYMAP_CODES: Record<number, KeyInfo> = Object.fromEntries(
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
export const KEYMAP_IDS: Map<string, KeyInfo> = 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
|
||||
])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user