mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-21 01:12:59 +00:00
feat: editing
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<mask id="cross" maskUnits="userSpaceOnUse">
|
|
||||||
<rect x="0" y="0" width="32" height="32" fill="white" />
|
|
||||||
<path d="M0 0L32 32M0 32L32 0" stroke="black" stroke-width="3" />
|
|
||||||
</mask>
|
|
||||||
<circle cx="16" cy="16" r="11.5" fill="none" stroke="white" stroke-width="9" mask="url(#cross)" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 433 B |
118
src/lib/assets/settings.yml
Normal file
118
src/lib/assets/settings.yml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
settings:
|
||||||
|
1:
|
||||||
|
title: Enable Serial Header
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
2:
|
||||||
|
title: Enable Serial Logging
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
3:
|
||||||
|
title: Enable Serial Debugging
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
4:
|
||||||
|
title: Enable Serial Raw
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
5:
|
||||||
|
title: Enable Serial Chord
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
6:
|
||||||
|
title: Enable Serial Keyboard
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
7:
|
||||||
|
title: Enable Serial Mouse
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
11:
|
||||||
|
title: Enable USB HID Keyboard
|
||||||
|
description: boolean 0 or 1, default is 1
|
||||||
|
12:
|
||||||
|
title: Enable Character Entry
|
||||||
|
description: boolean 0 or 1
|
||||||
|
13:
|
||||||
|
title: GUI-CTRL Swap Mode
|
||||||
|
description: boolean 0 or 1; 1 swaps keymap 0 and 1. (CCL only)
|
||||||
|
14:
|
||||||
|
title: Key Scan Duration
|
||||||
|
description: scan rate described in milliseconds; default is 2ms = 500Hz
|
||||||
|
15:
|
||||||
|
title: Key Debounce Press Duration
|
||||||
|
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
||||||
|
16:
|
||||||
|
title: Key Debounce Release Duration
|
||||||
|
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
||||||
|
17:
|
||||||
|
title: Keyboard Output Character Microsecond Delays
|
||||||
|
description: delay time in microseconds (one delay for press and again for release); default is 480us; max is 10240us; increments of 40us
|
||||||
|
21:
|
||||||
|
title: Enable USB HID Mouse
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
22:
|
||||||
|
title: Slow Mouse Speed
|
||||||
|
description: pixels to move at the mouse poll rate; default for CC1 is 5 = 250px/s
|
||||||
|
23:
|
||||||
|
title: Fast Mouse Speed
|
||||||
|
description: pixels to move at the mouse poll rate; default for CC1 is 25 = 1250px/s
|
||||||
|
24:
|
||||||
|
title: Enable Active Mouse
|
||||||
|
description: boolean 0 or 1; moves mouse back and forth every 60s
|
||||||
|
25:
|
||||||
|
title: Mouse Scroll Speed
|
||||||
|
description: default is 1; polls at 1/4th the rate of the mouse move updates
|
||||||
|
26:
|
||||||
|
title: Mouse Poll Duration
|
||||||
|
description: poll rate described in milliseconds; default is 20ms = 50Hz
|
||||||
|
31:
|
||||||
|
title: Enable Chording
|
||||||
|
description: boolean 0 or 1
|
||||||
|
32:
|
||||||
|
title: Enable Chording Character Counter Timeout
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
33:
|
||||||
|
title: Chording Character Counter Timeout Timer
|
||||||
|
description: 0-255 deciseconds; default is 40 or 4.0 seconds
|
||||||
|
34:
|
||||||
|
title: Chord Detection Press Tolerance(ms)
|
||||||
|
description: 1-50 milliseconds
|
||||||
|
35:
|
||||||
|
title: Chord Detection Release Tolerance(ms)
|
||||||
|
description: 1-50 milliseconds
|
||||||
|
41:
|
||||||
|
title: Enable Spurring
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
42:
|
||||||
|
title: Enable Spurring Character Counter Timeout
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
43:
|
||||||
|
title: Spurring Character Counter Timeout Timer
|
||||||
|
description: 0-255 seconds; default is 240
|
||||||
|
51:
|
||||||
|
title: Enable Arpeggiates
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
54:
|
||||||
|
title: Arpeggiate Tolerance
|
||||||
|
description: in milliseconds; default 800ms
|
||||||
|
61:
|
||||||
|
title: Enable Compound Chording (coming soon)
|
||||||
|
description: boolean 0 or 1; default is 0
|
||||||
|
64:
|
||||||
|
title: Compound Tolerance
|
||||||
|
description: in milliseconds; default 1500ms
|
||||||
|
81:
|
||||||
|
title: LED Brightness
|
||||||
|
description: 0-50 (CCL only); default is 5, which draws around 100 mA of current
|
||||||
|
82:
|
||||||
|
title: LED Color Code
|
||||||
|
description: Color Codes to be listed (CCL only)
|
||||||
|
83:
|
||||||
|
title: Enable LED Key Highlight (coming soon)
|
||||||
|
description: boolean 0 or 1 (CCL only)
|
||||||
|
84:
|
||||||
|
title: Enable LEDs
|
||||||
|
description: boolean 0 or 1; default is 1 (CCL only)
|
||||||
|
91:
|
||||||
|
title: Operating System
|
||||||
|
description: Operating system codes listed below
|
||||||
|
92:
|
||||||
|
title: Enable Realtime Feedback
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
93:
|
||||||
|
title: Enable CharaChorder Ready on startup
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {compileLayout} from "$lib/serialization/visual-layout"
|
import {compileLayout} from "$lib/serialization/visual-layout"
|
||||||
import type {VisualLayout, CompiledLayoutKey} from "$lib/serialization/visual-layout"
|
import type {VisualLayout, CompiledLayoutKey} from "$lib/serialization/visual-layout"
|
||||||
import {changes, layout} from "$lib/serial/connection"
|
import {deviceLayout} from "$lib/serial/connection"
|
||||||
import {dev} from "$app/environment"
|
import {dev} from "$app/environment"
|
||||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte"
|
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte"
|
||||||
import {getContext} from "svelte"
|
import {getContext} from "svelte"
|
||||||
import type {VisualLayoutConfig} from "./visual-layout.js"
|
import type {VisualLayoutConfig} from "./visual-layout.js"
|
||||||
|
import {changes, ChangeType} from "$lib/undo-redo"
|
||||||
|
|
||||||
const {scale, margin, strokeWidth, fontSize, iconFontSize} =
|
const {scale, margin, strokeWidth, fontSize, iconFontSize} =
|
||||||
getContext<VisualLayoutConfig>("visual-layout-config")
|
getContext<VisualLayoutConfig>("visual-layout-config")
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
const clickedGroup = groupParent.children.item(index) as SVGGElement
|
const clickedGroup = groupParent.children.item(index) as SVGGElement
|
||||||
const component = new ActionSelector({
|
const component = new ActionSelector({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {currentAction: get(layout)[get(activeLayer)][keyInfo.id]},
|
props: {currentAction: get(deviceLayout)[get(activeLayer)][keyInfo.id]},
|
||||||
})
|
})
|
||||||
const dialog = document.querySelector("dialog > div") as HTMLDivElement
|
const dialog = document.querySelector("dialog > div") as HTMLDivElement
|
||||||
const backdrop = document.querySelector("dialog") as HTMLDialogElement
|
const backdrop = document.querySelector("dialog") as HTMLDialogElement
|
||||||
@@ -152,7 +153,12 @@
|
|||||||
component.$on("close", closed)
|
component.$on("close", closed)
|
||||||
component.$on("select", ({detail}) => {
|
component.$on("select", ({detail}) => {
|
||||||
changes.update(changes => {
|
changes.update(changes => {
|
||||||
changes.push({layout: {[get(activeLayer)]: {[keyInfo.id]: detail}}})
|
changes.push({
|
||||||
|
type: ChangeType.Layout,
|
||||||
|
id: keyInfo.id,
|
||||||
|
layer: get(activeLayer),
|
||||||
|
action: detail,
|
||||||
|
})
|
||||||
return changes
|
return changes
|
||||||
})
|
})
|
||||||
closed()
|
closed()
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {layout} from "$lib/serial/connection"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
|
||||||
import {popup} from "$lib/popup"
|
|
||||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
|
||||||
|
|
||||||
export let id: number = 0
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
{#each $layout as layer, i}
|
|
||||||
<tr>
|
|
||||||
<th class="icon">counter_{i + 1}</th>
|
|
||||||
<ActionListItem id={layer[id]} />
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</table>
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {getActions} from "$lib/components/layout/get-actions.js"
|
|
||||||
import {changes, layout} from "$lib/serial/connection.js"
|
|
||||||
import {getContext} from "svelte"
|
import {getContext} from "svelte"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
|
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
|
||||||
import type {CompiledLayoutKey} from "$lib/serialization/visual-layout"
|
import type {CompiledLayoutKey} from "$lib/serialization/visual-layout"
|
||||||
|
import {layout} from "$lib/undo-redo.js"
|
||||||
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
|
|
||||||
const {fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize} =
|
const {fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize} =
|
||||||
getContext<VisualLayoutConfig>("visual-layout-config")
|
getContext<VisualLayoutConfig>("visual-layout-config")
|
||||||
@@ -21,27 +21,28 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each positions as position, layer}
|
{#each positions as position, layer}
|
||||||
{@const [action, changed] = getActions(layer, key.id, $layout, $changes)}
|
{@const {action: actionId, isApplied} = $layout[layer][key.id]}
|
||||||
|
{@const {code, icon, id} = KEYMAP_CODES[actionId] ?? {code: actionId}}
|
||||||
{@const isActive = layer === $activeLayer}
|
{@const isActive = layer === $activeLayer}
|
||||||
{@const direction = [(middle[0] - margin * 3) / position[0], (middle[1] - margin * 3) / position[1]]}
|
{@const direction = [(middle[0] - margin * 3) / position[0], (middle[1] - margin * 3) / position[1]]}
|
||||||
<text
|
<text
|
||||||
fill={changed ? "var(--md-sys-color-primary)" : "currentcolor"}
|
fill={isApplied ? "currentcolor" : "var(--md-sys-color-primary)"}
|
||||||
font-weight={changed ? "bold" : ""}
|
font-weight={isApplied ? "" : "bold"}
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
alignment-baseline="central"
|
alignment-baseline="central"
|
||||||
x={pos[0] + middle[0] + (changed ? fontSize / 3 : 0)}
|
x={pos[0] + middle[0] + (isApplied ? 0 : fontSize / 3)}
|
||||||
y={pos[1] + middle[1]}
|
y={pos[1] + middle[1]}
|
||||||
font-size={fontSizeMultiplier * (action.icon ? iconFontSize : fontSize)}
|
font-size={fontSizeMultiplier * (icon ? iconFontSize : fontSize)}
|
||||||
font-family={action.icon ? "Material Symbols Rounded" : undefined}
|
font-family={icon ? "Material Symbols Rounded" : undefined}
|
||||||
opacity={isActive ? 1 : inactiveOpacity}
|
opacity={isActive ? 1 : inactiveOpacity}
|
||||||
style:scale={isActive ? 1 : inactiveScale}
|
style:scale={isActive ? 1 : inactiveScale}
|
||||||
style:translate={isActive ? "0 0 0" : `${direction[0]}px ${direction[1]}px 0`}
|
style:translate={isActive ? "0 0 0" : `${direction[0]}px ${direction[1]}px 0`}
|
||||||
style:rotate="{rotate}deg"
|
style:rotate="{rotate}deg"
|
||||||
>
|
>
|
||||||
{#if action.code !== 0}
|
{#if code !== 0}
|
||||||
{action.icon || action.id || `0x${action.code?.toString(16)}`}
|
{icon || id || `0x${code.toString(16)}`}
|
||||||
{/if}
|
{/if}
|
||||||
{#if changed}
|
{#if !isApplied}
|
||||||
<tspan>•</tspan>
|
<tspan>•</tspan>
|
||||||
{/if}
|
{/if}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
{@const multiplier = 1.25}
|
{@const multiplier = 1.25}
|
||||||
<g style:transform="rotateZ({key.rotate}deg) translate({innerMargin}px, {innerMargin}px)">
|
<g style:transform="rotateZ({key.rotate}deg) translate({innerMargin}px, {innerMargin}px)">
|
||||||
<path
|
<path
|
||||||
opacity="0.4"
|
|
||||||
d="M{posX + p1},{posY} a{r1},{r1} 0 0,1 {-p1},{p1} l0,{-(p1 - p2)} a{r2},{r2} 0 0,0 {p2},{-p2}z"
|
d="M{posX + p1},{posY} a{r1},{r1} 0 0,1 {-p1},{p1} l0,{-(p1 - p2)} a{r2},{r2} 0 0,0 {p2},{-p2}z"
|
||||||
/>
|
/>
|
||||||
<KeyText
|
<KeyText
|
||||||
@@ -86,8 +85,8 @@
|
|||||||
|
|
||||||
path {
|
path {
|
||||||
fill: currentcolor;
|
fill: currentcolor;
|
||||||
fill-opacity: 0.2;
|
fill-opacity: 0;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
g:hover {
|
g:hover {
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<script>
|
|
||||||
import RingInput from "$lib/components/layout/RingInput.svelte"
|
|
||||||
|
|
||||||
export let activeLayer = 0
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col layout" style="gap: 0">
|
|
||||||
<div class="row" style="gap: 156px">
|
|
||||||
<div class="row">
|
|
||||||
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} />
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} />
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} />
|
|
||||||
</div>
|
|
||||||
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<RingInput {activeLayer} keys={{d: 60, w: 61, n: 62, e: 63, s: 64}} />
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 65, w: 66, n: 67, e: 68, s: 69}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 80, w: 81, n: 82, e: 83, s: 84}} />
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 70, w: 71, n: 72, e: 73, s: 74}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 85, w: 86, n: 87, e: 88, s: 89}} />
|
|
||||||
</div>
|
|
||||||
<RingInput {activeLayer} keys={{d: 75, w: 76, n: 77, e: 78, s: 79}} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row" style="gap: 48px; margin-top: -32px">
|
|
||||||
<RingInput {activeLayer} keys={{d: 10, e: 11, n: 12, w: 13, s: 14}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 55, w: 56, n: 57, e: 58, s: 59}} />
|
|
||||||
</div>
|
|
||||||
<div class="row" style="gap: 160px">
|
|
||||||
<RingInput {activeLayer} keys={{d: 5, e: 6, n: 7, w: 8, s: 9}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
|
||||||
</div>
|
|
||||||
<div class="row" style="gap: 320px; margin-top: -12px">
|
|
||||||
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.row,
|
|
||||||
.col {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {changes, highlightActions, layout} from "$lib/serial/connection"
|
|
||||||
import type {Change} from "$lib/serial/connection"
|
|
||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
|
||||||
import {editableLayout} from "$lib/editable-layout"
|
|
||||||
|
|
||||||
export let activeLayer = 0
|
|
||||||
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
|
|
||||||
|
|
||||||
const virtualLayerMap = [1, 0, 2]
|
|
||||||
const characterOffset = 8
|
|
||||||
|
|
||||||
function offsetDistance(quadrant: number, layer: number, activeLayer: number): number {
|
|
||||||
const layerOffsetIndex = virtualLayerMap[layer] - virtualLayerMap[activeLayer]
|
|
||||||
const layerOffset = quadrant > 2 ? -characterOffset : characterOffset
|
|
||||||
return 25 * quadrant + layerOffsetIndex * layerOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActions(id: number, layout: CharaLayout, changes: Change[]): [KeyInfo, KeyInfo | undefined][] {
|
|
||||||
return Array.from({length: 3}).map((_, i) => {
|
|
||||||
const actionId = layout?.[i][id]
|
|
||||||
const changedId = changes.findLast(it => it?.layout?.[i]?.[id] !== undefined)?.layout![i]![id]
|
|
||||||
if (changedId !== undefined) {
|
|
||||||
return [KEYMAP_CODES[changedId], KEYMAP_CODES[actionId]]
|
|
||||||
} else {
|
|
||||||
return [KEYMAP_CODES[actionId], undefined]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="radial">
|
|
||||||
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
|
||||||
{@const actions = getActions(id, $layout, $changes)}
|
|
||||||
<button
|
|
||||||
use:editableLayout={{activeLayer, id}}
|
|
||||||
class:active={actions.some(([it]) => it && $highlightActions?.includes(it.code))}
|
|
||||||
>
|
|
||||||
{#each actions as [keyInfo, old], layer}
|
|
||||||
{#if keyInfo}
|
|
||||||
<span
|
|
||||||
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
|
||||||
class:icon={!!keyInfo.icon}
|
|
||||||
class:changed={!!old}
|
|
||||||
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
|
||||||
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use "sass:math";
|
|
||||||
|
|
||||||
$border-width: 18px;
|
|
||||||
$gap: 6px;
|
|
||||||
$size: 96;
|
|
||||||
$offset: 14;
|
|
||||||
$scale-difference: 0.2;
|
|
||||||
$transition-time: 750ms;
|
|
||||||
|
|
||||||
.radial {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
container: radial / size;
|
|
||||||
|
|
||||||
width: #{$size * 1px};
|
|
||||||
height: #{$size * 1px};
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
$cr: math.div($size, 2) - 2 * $offset;
|
|
||||||
|
|
||||||
will-change: scale, offset-distance;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
scale: 0.9;
|
|
||||||
offset-path: path(
|
|
||||||
"M#{math.div($size, 2)} #{$offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$size - $offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$offset}Z"
|
|
||||||
);
|
|
||||||
offset-rotate: 0deg;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
opacity: 0.2;
|
|
||||||
|
|
||||||
transition:
|
|
||||||
scale $transition-time ease,
|
|
||||||
opacity $transition-time ease,
|
|
||||||
offset-distance $transition-time ease;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
scale: 1;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.changed {
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
|
||||||
background: var(--md-sys-color-secondary-container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
all: unset;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
|
|
||||||
width: 100cqw;
|
|
||||||
height: 100cqh;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
|
|
||||||
mask-image: url("$lib/assets/quater-ring.svg");
|
|
||||||
mask-size: 100% 100%;
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(1) {
|
|
||||||
clip-path: polygon(50% 50%, 0 0, 100% 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
clip-path: polygon(50% 50%, 100% 0, 100% 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
clip-path: polygon(50% 50%, 0 100%, 100% 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(4) {
|
|
||||||
clip-path: polygon(50% 50%, 0 0, 0 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
translate: -50% -50%;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
width: 25cqw;
|
|
||||||
height: 25cqh;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
mask-image: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
|
||||||
import type {Change} from "$lib/serial/connection"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
|
||||||
|
|
||||||
export function getActions(
|
|
||||||
layer: number,
|
|
||||||
id: number,
|
|
||||||
layout: CharaLayout,
|
|
||||||
changes: Change[],
|
|
||||||
): [KeyInfo, boolean] {
|
|
||||||
const actionId = layout?.[layer][id]
|
|
||||||
const changedId = changes.findLast(it => it?.layout?.[layer]?.[id] !== undefined)?.layout?.[layer]?.[id]
|
|
||||||
if (changedId !== undefined) {
|
|
||||||
return [KEYMAP_CODES[changedId] ?? {code: changedId}, true]
|
|
||||||
} else {
|
|
||||||
return [KEYMAP_CODES[actionId] ?? {code: actionId}, false]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import type {Action} from "svelte/action"
|
|
||||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
|
||||||
import {changes, layout} from "$lib/serial/connection"
|
|
||||||
import {get} from "svelte/store"
|
|
||||||
|
|
||||||
export const editableLayout: Action<HTMLButtonElement, {activeLayer: number; id: number}> = (
|
|
||||||
node,
|
|
||||||
{id, activeLayer},
|
|
||||||
) => {
|
|
||||||
let component: ActionSelector | undefined
|
|
||||||
function present() {
|
|
||||||
component?.$destroy()
|
|
||||||
component = new ActionSelector({
|
|
||||||
target: document.body,
|
|
||||||
props: {currentAction: get(layout)[activeLayer][id]},
|
|
||||||
})
|
|
||||||
component.$on("close", () => {
|
|
||||||
component!.$destroy()
|
|
||||||
})
|
|
||||||
component.$on("select", ({detail}) => {
|
|
||||||
changes.update(changes => {
|
|
||||||
changes.push({layout: {[activeLayer]: {[id]: detail}}})
|
|
||||||
return changes
|
|
||||||
})
|
|
||||||
component!.$destroy()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
node.addEventListener("click", present)
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
node.removeEventListener("click", present)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import type {Writable} from "svelte/store"
|
|||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
import type {CharaLayout} from "$lib/serialization/layout"
|
||||||
import {persistentWritable} from "$lib/storage"
|
import {persistentWritable} from "$lib/storage"
|
||||||
import {userPreferences} from "$lib/preferences"
|
import {userPreferences} from "$lib/preferences"
|
||||||
|
import settingInfo from "$lib/assets/settings.yml"
|
||||||
|
|
||||||
export const serialPort = writable<CharaDevice | undefined>()
|
export const serialPort = writable<CharaDevice | undefined>()
|
||||||
|
|
||||||
@@ -15,27 +16,32 @@ export interface SerialLogEntry {
|
|||||||
|
|
||||||
export const serialLog = writable<SerialLogEntry[]>([])
|
export const serialLog = writable<SerialLogEntry[]>([])
|
||||||
|
|
||||||
export const chords = persistentWritable<Chord[]>("chord-library", [], () => get(userPreferences).backup)
|
/**
|
||||||
|
* Chords as read from the device
|
||||||
|
*/
|
||||||
|
export const deviceChords = persistentWritable<Chord[]>(
|
||||||
|
"chord-library",
|
||||||
|
[],
|
||||||
|
() => get(userPreferences).backup,
|
||||||
|
)
|
||||||
|
|
||||||
export const layout = persistentWritable<CharaLayout>(
|
/**
|
||||||
|
* Layout as read from the device
|
||||||
|
*/
|
||||||
|
export const deviceLayout = persistentWritable<CharaLayout>(
|
||||||
"layout",
|
"layout",
|
||||||
[[], [], []],
|
[[], [], []],
|
||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface Change {
|
/**
|
||||||
layout?: Record<number, Record<number, number>>
|
* Settings as read from the device
|
||||||
chords?: Array<Record<"delete" | "edit" | "add", Chord>>
|
*/
|
||||||
settings?: Record<number, number>
|
export const deviceSettings = persistentWritable<number[]>(
|
||||||
}
|
"device-settings",
|
||||||
|
[],
|
||||||
export const changes = persistentWritable<Change[]>("changes", [])
|
() => get(userPreferences).backup,
|
||||||
|
)
|
||||||
export const settings = writable({})
|
|
||||||
|
|
||||||
export const unsavedChanges = writable(new Map<number, number>())
|
|
||||||
|
|
||||||
export const highlightActions: Writable<number[]> = writable([])
|
|
||||||
|
|
||||||
export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done")
|
export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done")
|
||||||
|
|
||||||
@@ -45,19 +51,28 @@ export async function initSerial(manual = false) {
|
|||||||
serialPort.set(device)
|
serialPort.set(device)
|
||||||
|
|
||||||
syncStatus.set("downloading")
|
syncStatus.set("downloading")
|
||||||
|
const parsedSettings: number[] = []
|
||||||
|
for (const key in settingInfo.settings) {
|
||||||
|
try {
|
||||||
|
parsedSettings[Number.parseInt(key)] = await device.getSetting(Number.parseInt(key))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
deviceSettings.set(parsedSettings)
|
||||||
|
|
||||||
const parsedLayout: CharaLayout = [[], [], []]
|
const parsedLayout: CharaLayout = [[], [], []]
|
||||||
for (let layer = 1; layer <= 3; layer++) {
|
for (let layer = 1; layer <= 3; layer++) {
|
||||||
|
// TODO: this will fail for LITE!
|
||||||
for (let i = 0; i < 90; i++) {
|
for (let i = 0; i < 90; i++) {
|
||||||
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i)
|
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layout.set(parsedLayout)
|
deviceLayout.set(parsedLayout)
|
||||||
|
|
||||||
const chordCount = await device.getChordCount()
|
const chordCount = await device.getChordCount()
|
||||||
const chordInfo = []
|
const chordInfo = []
|
||||||
for (let i = 0; i < chordCount; i++) {
|
for (let i = 0; i < chordCount; i++) {
|
||||||
chordInfo.push(await device.getChord(i))
|
chordInfo.push(await device.getChord(i))
|
||||||
}
|
}
|
||||||
chords.set(chordInfo)
|
deviceChords.set(chordInfo)
|
||||||
syncStatus.set("done")
|
syncStatus.set("done")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export class CharaDevice {
|
|||||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteChord(chord: Chord) {
|
async deleteChord(chord: Pick<Chord, "actions">) {
|
||||||
const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`)
|
const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`)
|
||||||
if (status.at(-1) !== "0") throw new Error(`Failed with status ${status}`)
|
if (status.at(-1) !== "0") throw new Error(`Failed with status ${status}`)
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,8 @@ export class CharaDevice {
|
|||||||
* @param action the assigned action id
|
* @param action the assigned action id
|
||||||
*/
|
*/
|
||||||
async setLayoutKey(layer: number, id: number, action: number) {
|
async setLayoutKey(layer: number, id: number, action: number) {
|
||||||
const [status] = await this.send(`VAR B3 A${layer} ${id} ${action}`)
|
const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`)
|
||||||
|
console.log(status)
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type {Action} from "svelte/action"
|
import type {Action} from "svelte/action"
|
||||||
import {serialPort, unsavedChanges} from "$lib/serial/connection"
|
import {changes, ChangeType, settings} from "$lib/undo-redo"
|
||||||
import {get} from "svelte/store"
|
|
||||||
|
|
||||||
export const setting: Action<HTMLInputElement, {id: number; inverse?: number; scale?: number}> = function (
|
export const setting: Action<HTMLInputElement, {id: number; inverse?: number; scale?: number}> = function (
|
||||||
node: HTMLInputElement,
|
node: HTMLInputElement,
|
||||||
@@ -9,15 +8,20 @@ export const setting: Action<HTMLInputElement, {id: number; inverse?: number; sc
|
|||||||
node.setAttribute("disabled", "")
|
node.setAttribute("disabled", "")
|
||||||
const type = node.getAttribute("type") as "number" | "checkbox"
|
const type = node.getAttribute("type") as "number" | "checkbox"
|
||||||
|
|
||||||
const unsubscribe = serialPort.subscribe(async port => {
|
const unsubscribe = settings.subscribe(async settings => {
|
||||||
if (port) {
|
if (id in settings) {
|
||||||
|
const {value, isApplied} = settings[id]
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
const value = Number(await port.getSetting(id).then(it => it.toString()))
|
|
||||||
node.value = (
|
node.value = (
|
||||||
inverse !== undefined ? inverse / value : scale !== undefined ? scale * value : value
|
inverse !== undefined ? inverse / value : scale !== undefined ? scale * value : value
|
||||||
).toString()
|
).toString()
|
||||||
} else {
|
} else {
|
||||||
node.checked = await port.getSetting(id).then(it => it !== 0)
|
node.checked = value !== 0
|
||||||
|
}
|
||||||
|
if (isApplied) {
|
||||||
|
node.classList.remove("pending-changes")
|
||||||
|
} else {
|
||||||
|
node.classList.add("pending-changes")
|
||||||
}
|
}
|
||||||
node.removeAttribute("disabled")
|
node.removeAttribute("disabled")
|
||||||
} else {
|
} else {
|
||||||
@@ -26,8 +30,7 @@ export const setting: Action<HTMLInputElement, {id: number; inverse?: number; sc
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function listener(event: Event) {
|
async function listener(event: Event) {
|
||||||
const currentValue = await get(serialPort)!.getSetting(id)
|
let value: number
|
||||||
let value = 0
|
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
value = Number((event as InputEvent).data)
|
value = Number((event as InputEvent).data)
|
||||||
if (Number.isNaN(value)) return
|
if (Number.isNaN(value)) return
|
||||||
@@ -35,16 +38,14 @@ export const setting: Action<HTMLInputElement, {id: number; inverse?: number; sc
|
|||||||
} else {
|
} else {
|
||||||
value = node.checked ? 1 : 0
|
value = node.checked ? 1 : 0
|
||||||
}
|
}
|
||||||
await get(serialPort)!.setSetting(id, value)
|
|
||||||
|
|
||||||
const originalValue = get(unsavedChanges).get(id)
|
changes.update(changes => {
|
||||||
unsavedChanges.update(it => {
|
changes.push({
|
||||||
if (originalValue === value) {
|
type: ChangeType.Setting,
|
||||||
it.delete(id)
|
id: id,
|
||||||
} else if (!it.has(id)) {
|
setting: value,
|
||||||
it.set(id, currentValue)
|
})
|
||||||
}
|
return changes
|
||||||
return it
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
node.addEventListener("input", listener)
|
node.addEventListener("input", listener)
|
||||||
|
|||||||
109
src/lib/undo-redo.ts
Normal file
109
src/lib/undo-redo.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import {persistentWritable} from "$lib/storage"
|
||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {serializeActions} from "$lib/serial/chord"
|
||||||
|
import type {Chord} from "$lib/serial/chord"
|
||||||
|
import {deviceChords, deviceLayout, deviceSettings} from "$lib/serial/connection"
|
||||||
|
|
||||||
|
export enum ChangeType {
|
||||||
|
Layout,
|
||||||
|
Chord,
|
||||||
|
Setting,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LayoutChange {
|
||||||
|
type: ChangeType.Layout
|
||||||
|
id: number
|
||||||
|
layer: number
|
||||||
|
action: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChordChange {
|
||||||
|
type: ChangeType.Chord
|
||||||
|
actions: number[]
|
||||||
|
phrase: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingChange {
|
||||||
|
type: ChangeType.Setting
|
||||||
|
id: number
|
||||||
|
setting: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeInfo {
|
||||||
|
isApplied: boolean
|
||||||
|
isCommitted?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Change = LayoutChange | ChordChange | SettingChange
|
||||||
|
|
||||||
|
export const changes = persistentWritable<Change[]>("changes", [])
|
||||||
|
|
||||||
|
export interface Overlay {
|
||||||
|
layout: [Map<number, number>, Map<number, number>, Map<number, number>]
|
||||||
|
chords: Map<bigint, number[]>
|
||||||
|
settings: Map<number, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const overlay = derived(changes, changes => {
|
||||||
|
console.time("overlay building")
|
||||||
|
const overlay: Overlay = {
|
||||||
|
layout: [new Map(), new Map(), new Map()],
|
||||||
|
chords: new Map(),
|
||||||
|
settings: new Map(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
switch (change.type) {
|
||||||
|
case ChangeType.Layout:
|
||||||
|
overlay.layout[change.layer].set(change.id, change.action)
|
||||||
|
break
|
||||||
|
case ChangeType.Chord:
|
||||||
|
overlay.chords.set(serializeActions(change.actions), change.phrase)
|
||||||
|
break
|
||||||
|
case ChangeType.Setting:
|
||||||
|
overlay.settings.set(change.id, change.setting)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.timeEnd("overlay building")
|
||||||
|
|
||||||
|
return overlay
|
||||||
|
})
|
||||||
|
|
||||||
|
export const settings = derived([overlay, deviceSettings], ([overlay, settings]) =>
|
||||||
|
settings.map<{value: number} & ChangeInfo>((value, id) => ({
|
||||||
|
value: overlay.settings.get(id) ?? value,
|
||||||
|
isApplied: !overlay.settings.has(id),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
export type KeyInfo = {action: number} & ChangeInfo
|
||||||
|
export const layout = derived([overlay, deviceLayout], ([overlay, layout]) =>
|
||||||
|
layout.map(
|
||||||
|
(actions, layer) =>
|
||||||
|
actions.map<KeyInfo>((action, id) => ({
|
||||||
|
action: overlay.layout[layer].get(id) ?? action,
|
||||||
|
isApplied: !overlay.layout[layer].has(id),
|
||||||
|
})) as [KeyInfo, KeyInfo, KeyInfo],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ChordInfo = Chord & ChangeInfo
|
||||||
|
export const chords = derived([overlay, deviceChords], ([overlay, chords]) =>
|
||||||
|
chords.map<ChordInfo>(chord => {
|
||||||
|
const key = serializeActions(chord.actions)
|
||||||
|
if (overlay.chords.has(key)) {
|
||||||
|
return {
|
||||||
|
actions: chord.actions,
|
||||||
|
phrase: overlay.chords.get(key)!,
|
||||||
|
isApplied: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
actions: chord.actions,
|
||||||
|
phrase: chord.phrase,
|
||||||
|
isApplied: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
1
src/lib/versioning.ts
Normal file
1
src/lib/versioning.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// TODO
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {parseCompressed, stringifyCompressed} from "$lib/serial/serialization"
|
import {parseCompressed, stringifyCompressed} from "$lib/serial/serialization"
|
||||||
import {chords, layout} from "$lib/serial/connection"
|
import {deviceChords, deviceLayout} from "$lib/serial/connection"
|
||||||
import {preference} from "$lib/preferences"
|
import {preference} from "$lib/preferences"
|
||||||
import type {Chord} from "$lib/serial/chord"
|
import type {Chord} from "$lib/serial/chord"
|
||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
import type {CharaLayout} from "$lib/serialization/layout"
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
const downloadUrl = URL.createObjectURL(
|
const downloadUrl = URL.createObjectURL(
|
||||||
await stringifyCompressed({
|
await stringifyCompressed({
|
||||||
isCharaBackup: "v1.0",
|
isCharaBackup: "v1.0",
|
||||||
chords: $chords,
|
chords: $deviceChords,
|
||||||
layout: $layout,
|
layout: $deviceLayout,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const element = document.createElement("a")
|
const element = document.createElement("a")
|
||||||
@@ -34,10 +34,10 @@
|
|||||||
const backup = await parseCompressed<Backup>(input)
|
const backup = await parseCompressed<Backup>(input)
|
||||||
if (backup.isCharaBackup !== "v1.0") throw new Error("Invalid Backup")
|
if (backup.isCharaBackup !== "v1.0") throw new Error("Invalid Backup")
|
||||||
if (backup.chords) {
|
if (backup.chords) {
|
||||||
$chords = backup.chords
|
$deviceChords = backup.chords
|
||||||
}
|
}
|
||||||
if (backup.layout) {
|
if (backup.layout) {
|
||||||
$layout = backup.layout
|
$deviceLayout = backup.layout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LL from "../i18n/i18n-svelte"
|
import LL from "../i18n/i18n-svelte"
|
||||||
import {changes} from "$lib/serial/connection"
|
import {changes, ChangeType, chords, layout, settings} from "$lib/undo-redo"
|
||||||
import type {Change} from "$lib/serial/connection"
|
import type {Change} from "$lib/undo-redo"
|
||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
import {action} from "$lib/title"
|
import {action} from "$lib/title"
|
||||||
|
import {deviceChords, deviceLayout, deviceSettings, serialPort, syncStatus} from "$lib/serial/connection"
|
||||||
|
|
||||||
function undo() {
|
function undo() {
|
||||||
redoQueue = [$changes.pop()!, ...redoQueue]
|
redoQueue = [$changes.pop()!, ...redoQueue]
|
||||||
@@ -20,8 +21,54 @@
|
|||||||
}
|
}
|
||||||
let redoQueue: Change[] = []
|
let redoQueue: Change[] = []
|
||||||
|
|
||||||
function apply() {
|
async function apply() {
|
||||||
// TODO
|
const port = $serialPort
|
||||||
|
if (!port) return
|
||||||
|
|
||||||
|
$syncStatus = "uploading"
|
||||||
|
for (const change of $changes) {
|
||||||
|
switch (change.type) {
|
||||||
|
case ChangeType.Layout:
|
||||||
|
await port.setLayoutKey(change.layer + 1, change.id, change.action)
|
||||||
|
break
|
||||||
|
case ChangeType.Chord:
|
||||||
|
if (change.phrase) {
|
||||||
|
await port.setChord({actions: change.actions, phrase: change.phrase})
|
||||||
|
} else {
|
||||||
|
await port.deleteChord({actions: change.actions})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ChangeType.Setting:
|
||||||
|
await port.setSetting(change.id, change.setting)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$deviceLayout = $layout.map(layer => layer.map<number>(({action}) => action)) as [
|
||||||
|
number[],
|
||||||
|
number[],
|
||||||
|
number[],
|
||||||
|
]
|
||||||
|
$deviceChords = $chords.map(({actions, phrase}) => ({actions, phrase}))
|
||||||
|
$deviceSettings = $settings.map(({value}) => value)
|
||||||
|
$changes = []
|
||||||
|
$syncStatus = "done"
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flashChanges() {
|
||||||
|
$syncStatus = "uploading"
|
||||||
|
// Yes, this is a completely arbitrary and unnecessary delay.
|
||||||
|
// The only purpose of it is to create a sense of weight,
|
||||||
|
// aka make it more "energy intensive" to click.
|
||||||
|
// The only conceivable way users could reach the commit limit in this case
|
||||||
|
// would be if they click it every time they change a setting.
|
||||||
|
// Because of that, we don't need to show a fearmongering message such as
|
||||||
|
// "Your device will break after you click this 10,000 times!"
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 6000))
|
||||||
|
if ($serialPort) {
|
||||||
|
await $serialPort.commit()
|
||||||
|
$changes = []
|
||||||
|
}
|
||||||
|
$syncStatus = "done"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -107,7 +154,11 @@
|
|||||||
on:click={redo}>redo</button
|
on:click={redo}>redo</button
|
||||||
>
|
>
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
<button use:action={{title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s"}} class="icon">save</button>
|
<button
|
||||||
|
use:action={{title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s"}}
|
||||||
|
on:click={flashChanges}
|
||||||
|
class="icon">save</button
|
||||||
|
>
|
||||||
{#if $changes.length !== 0}
|
{#if $changes.length !== 0}
|
||||||
<button
|
<button
|
||||||
class="click-me"
|
class="click-me"
|
||||||
@@ -121,73 +172,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.pacman {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
aspect-ratio: 1;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
background: currentcolor;
|
|
||||||
border: 8px solid currentcolor;
|
|
||||||
border-radius: 100%;
|
|
||||||
outline: 6px solid currentcolor;
|
|
||||||
outline-offset: 2px;
|
|
||||||
|
|
||||||
animation: pacman 0.2s linear infinite alternate-reverse;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 25%;
|
|
||||||
|
|
||||||
width: 200%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-background);
|
|
||||||
animation: squish 0.2s linear infinite alternate-reverse;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "c c o s";
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
width: 500%;
|
|
||||||
animation: go 1s linear infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes go {
|
|
||||||
from {
|
|
||||||
translate: 0 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
translate: -100% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes squish {
|
|
||||||
from {
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
scale: 1 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pacman {
|
|
||||||
to {
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
scale: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-me {
|
.click-me {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {serialPort, syncStatus, unsavedChanges} from "$lib/serial/connection"
|
import {serialPort, syncStatus} from "$lib/serial/connection"
|
||||||
import {slide, fly} from "svelte/transition"
|
import {slide, fly} from "svelte/transition"
|
||||||
import {canShare, triggerShare} from "$lib/share"
|
import {canShare, triggerShare} from "$lib/share"
|
||||||
import {popup} from "$lib/popup"
|
import {popup} from "$lib/popup"
|
||||||
@@ -13,24 +13,6 @@
|
|||||||
import ConfigTabs from "./ConfigTabs.svelte"
|
import ConfigTabs from "./ConfigTabs.svelte"
|
||||||
import EditActions from "./EditActions.svelte"
|
import EditActions from "./EditActions.svelte"
|
||||||
|
|
||||||
async function flashChanges() {
|
|
||||||
$syncStatus = "uploading"
|
|
||||||
// Yes, this is a completely arbitrary and unnecessary delay.
|
|
||||||
// The only purpose of it is to create a sense of weight,
|
|
||||||
// aka make it more "energy intensive" to click.
|
|
||||||
// The only conceivable way users could reach the commit limit in this case
|
|
||||||
// would be if they click it every time they change a setting.
|
|
||||||
// Because of that, we don't need to show a fearmongering message such as
|
|
||||||
// "Your device will break after you click this 10,000 times!"
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 6000))
|
|
||||||
$serialPort.commit()
|
|
||||||
unsavedChanges.update(it => {
|
|
||||||
it.clear()
|
|
||||||
return it
|
|
||||||
})
|
|
||||||
$syncStatus = "done"
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (browser && !canAutoConnect()) {
|
$: if (browser && !canAutoConnect()) {
|
||||||
connectButton?.click()
|
connectButton?.click()
|
||||||
}
|
}
|
||||||
@@ -55,17 +37,6 @@
|
|||||||
<PwaStatus />
|
<PwaStatus />
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
{#if $unsavedChanges.size > 0}
|
|
||||||
<button
|
|
||||||
disabled={$syncStatus === "uploading"}
|
|
||||||
on:click={flashChanges}
|
|
||||||
transition:fly={{x: -8}}
|
|
||||||
title={$LL.deviceManager.APPLY_SETTINGS()}
|
|
||||||
class="icon"
|
|
||||||
>save
|
|
||||||
</button>
|
|
||||||
<div transition:slide class="separator" />
|
|
||||||
{/if}
|
|
||||||
{#if $serialPort}
|
{#if $serialPort}
|
||||||
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
|
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||||
{#if $syncStatus === "downloading"}
|
{#if $syncStatus === "downloading"}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {changes, chords} from "$lib/serial/connection"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import Index from "flexsearch"
|
import Index from "flexsearch"
|
||||||
import type {Chord} from "$lib/serial/chord"
|
|
||||||
import LL from "../../../i18n/i18n-svelte"
|
import LL from "../../../i18n/i18n-svelte"
|
||||||
import {action} from "$lib/title"
|
import {action} from "$lib/title"
|
||||||
import {onDestroy, onMount} from "svelte"
|
import {onDestroy, onMount} from "svelte"
|
||||||
import ActionStringEdit from "$lib/components/ActionStringEdit.svelte"
|
import ActionStringEdit from "$lib/components/ActionStringEdit.svelte"
|
||||||
|
import {changes, ChangeType, chords} from "$lib/undo-redo"
|
||||||
|
import type {ChordInfo} from "$lib/undo-redo"
|
||||||
|
import {derived, writable} from "svelte/store"
|
||||||
|
|
||||||
const resultSize = 38
|
const resultSize = 38
|
||||||
let results: HTMLElement
|
let results: HTMLElement
|
||||||
let pageSize: number
|
const pageSize = writable(0)
|
||||||
let resizeObserver: ResizeObserver
|
let resizeObserver: ResizeObserver
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
resizeObserver = new ResizeObserver(() => {
|
resizeObserver = new ResizeObserver(() => {
|
||||||
pageSize = Math.floor(results.clientHeight / resultSize)
|
pageSize.set(Math.floor(results.clientHeight / resultSize))
|
||||||
})
|
})
|
||||||
pageSize = Math.floor(results.clientHeight / resultSize)
|
pageSize.set(Math.floor(results.clientHeight / resultSize))
|
||||||
resizeObserver.observe(results)
|
resizeObserver.observe(results)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -27,27 +28,33 @@
|
|||||||
|
|
||||||
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
||||||
|
|
||||||
function buildIndex(chords: Chord[]): Index {
|
function buildIndex(chords: ChordInfo[]): Index {
|
||||||
const index = new Index({tokenize: "full"})
|
const index = new Index({tokenize: "full"})
|
||||||
chords.forEach((chord, i) => {
|
chords.forEach((chord, i) => {
|
||||||
index.add(i, chord.phrase.map(it => KEYMAP_CODES[it].id).join(""))
|
if ("phrase" in chord) {
|
||||||
|
index.add(i, chord.phrase.map(it => KEYMAP_CODES[it].id).join(""))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchFilter: number[] | undefined
|
const searchFilter = writable<number[] | undefined>(undefined)
|
||||||
|
|
||||||
function search(event: Event) {
|
function search(event: Event) {
|
||||||
const query = (event.target as HTMLInputElement).value
|
const query = (event.target as HTMLInputElement).value
|
||||||
searchFilter = query && searchIndex ? searchIndex.search(query) : undefined
|
searchFilter.set(query && searchIndex ? searchIndex.search(query) : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
|
const items = derived([searchFilter, chords], ([filter, chords]) =>
|
||||||
$: lastPage = Math.ceil(items.length / pageSize) - 1
|
(filter?.map(it => [chords[it], it] as const) ?? chords.map((it, i) => [it, i] as const)).filter(
|
||||||
|
([{phrase}]) => phrase.length > 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const lastPage = derived([items, pageSize], ([items, pageSize]) => Math.ceil(items.length / pageSize) - 1)
|
||||||
|
|
||||||
let page = 0
|
let page = 0
|
||||||
$: {
|
$: {
|
||||||
items
|
$items
|
||||||
page = 0
|
page = 0
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -63,8 +70,8 @@
|
|||||||
on:input={search}
|
on:input={search}
|
||||||
/>
|
/>
|
||||||
<div class="paginator">
|
<div class="paginator">
|
||||||
{#if lastPage !== -1}
|
{#if $lastPage !== -1}
|
||||||
{page + 1} / {lastPage + 1}
|
{page + 1} / {$lastPage + 1}
|
||||||
{:else}
|
{:else}
|
||||||
- / -
|
- / -
|
||||||
{/if}
|
{/if}
|
||||||
@@ -74,26 +81,31 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="icon"
|
class="icon"
|
||||||
on:click={() => (page = Math.min(page + 1, lastPage))}
|
on:click={() => (page = Math.min(page + 1, $lastPage))}
|
||||||
use:action={{shortcut: "ctrl+right"}}>navigate_next</button
|
use:action={{shortcut: "ctrl+right"}}>navigate_next</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section bind:this={results}>
|
<section bind:this={results}>
|
||||||
<table>
|
<table>
|
||||||
{#if lastPage !== -1}
|
{#if $lastPage !== -1}
|
||||||
{#each items.slice(page * pageSize, (page + 1) * pageSize) as [chord]}
|
{#each $items.slice(page * $pageSize, (page + 1) * $pageSize) as [{ actions, phrase, isApplied }]}
|
||||||
<tr>
|
<tr style:color={isApplied ? "" : "var(--md-sys-color-secondary"}>
|
||||||
<th>
|
<th>
|
||||||
<ActionStringEdit actions={chord.phrase} />
|
<ActionStringEdit {actions} />
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
<ActionStringEdit actions={chord.actions} />
|
<ActionStringEdit actions={phrase} />
|
||||||
</td>
|
</td>
|
||||||
<td class="table-buttons">
|
<td class="table-buttons">
|
||||||
<button class="icon compact">share</button>
|
<button class="icon compact">share</button>
|
||||||
<button class="icon compact" on:click={() => $changes.push({chords: [{delete: chord}]})}
|
<button
|
||||||
>delete</button
|
class="icon compact"
|
||||||
|
on:click={() =>
|
||||||
|
changes.update(changes => {
|
||||||
|
changes.push({type: ChangeType.Chord, actions, phrase: []})
|
||||||
|
return changes
|
||||||
|
})}>delete</button
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {share} from "$lib/share"
|
import {share} from "$lib/share"
|
||||||
import {layout} from "$lib/serial/connection"
|
import {deviceLayout} from "$lib/serial/connection"
|
||||||
import tippy from "tippy.js"
|
import tippy from "tippy.js"
|
||||||
import {onMount, setContext} from "svelte"
|
import {onMount, setContext} from "svelte"
|
||||||
import Layout from "$lib/components/layout/Layout.svelte"
|
import Layout from "$lib/components/layout/Layout.svelte"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
if (url.searchParams.has("import")) {
|
if (url.searchParams.has("import")) {
|
||||||
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
|
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
|
||||||
if (file.type === "layout") {
|
if (file.type === "layout") {
|
||||||
$layout = file.layout
|
$deviceLayout = file.layout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
charaVersion: 1,
|
charaVersion: 1,
|
||||||
type: "layout",
|
type: "layout",
|
||||||
device: "one",
|
device: "one",
|
||||||
layout: $layout,
|
layout: $deviceLayout,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await navigator.clipboard.writeText(url.toString())
|
await navigator.clipboard.writeText(url.toString())
|
||||||
@@ -52,7 +52,8 @@
|
|||||||
const file = await fileInput.files?.item(0)?.text()
|
const file = await fileInput.files?.item(0)?.text()
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const importedLayout = isCsvLayout(file) ? csvLayoutToJson(file) : (JSON.parse(file) as CharaLayoutFile)
|
const importedLayout = isCsvLayout(file) ? csvLayoutToJson(file) : (JSON.parse(file) as CharaLayoutFile)
|
||||||
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1) $layout = importedLayout.layout
|
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1)
|
||||||
|
$deviceLayout = importedLayout.layout
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext<VisualLayoutConfig>("visual-layout-config", {
|
setContext<VisualLayoutConfig>("visual-layout-config", {
|
||||||
|
|||||||
@@ -225,4 +225,8 @@
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form label:has(:global(.pending-changes)) {
|
||||||
|
color: var(--md-sys-color-tertiary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user