mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-17 07:22:50 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
5fa4b1fd09
|
|||
|
f585a0ebda
|
|||
|
a48e2b5a16
|
|||
|
fd612eda1d
|
|||
|
a1fe6f7110
|
|||
|
0e57e810e0
|
|||
|
a15d5dde38
|
|||
|
560206129e
|
|||
|
cb7c70dac1
|
|||
|
edabf8ec84
|
|||
|
f2f61f32f2
|
|||
|
a3857843d6
|
|||
|
c1b1068c4b
|
|||
|
2411dd2bea
|
|||
|
7911904906
|
|||
|
630687de80
|
|||
|
84b22e0006
|
|||
|
dd070c8856
|
|||
|
6872cd0554
|
|||
|
628007af23
|
|||
|
19fad84357
|
|||
|
f172318a78
|
|||
|
c2e3850082
|
|||
|
7a5a4eb434
|
|||
|
c878311f62
|
|||
|
fb3fb246e9
|
|||
|
b4e4ca84a4
|
|||
|
c1b1544256
|
|||
|
03dd528465
|
|||
|
81af9f2e82
|
|||
|
6bb42429e5
|
|||
|
d07751a944
|
|||
|
8867030ede
|
|||
|
faaa6dd5be
|
|||
|
43cf13094e
|
|||
|
ed523628ff
|
|||
|
98b451eec9
|
|||
|
6e37dc198f
|
|||
|
e319b1bfaf
|
|||
|
eb33b64100
|
|||
|
766bc44a85
|
|||
|
b679aa377a
|
|||
|
ea3192d4e6
|
|||
|
256daec412
|
|||
|
29a07133d1
|
|||
|
c3bd8431e5
|
|||
|
c8e04ed6cc
|
|||
|
d98653995b
|
|||
|
3dd9611ebf
|
|||
|
9d9360375b
|
|||
|
d683c8c70c
|
|||
|
d8d430f333
|
|||
|
fe850f47ec
|
|||
|
f9a63a8724
|
|||
|
af01426f43
|
|||
|
9d7cefb3b4
|
|||
|
f44e5a79de
|
|||
|
8b2e92c124
|
|||
|
f758be91a9
|
|||
|
bf4c86e698
|
|||
|
50a09d2008
|
|||
|
3c1a4de4a7
|
|||
|
8cbdf1393f
|
|||
|
1ccb17f053
|
|||
|
532dc70fe2
|
|||
|
d5893013f9
|
|||
|
80308cad73
|
|||
|
2d59bd016f
|
|||
|
298de49257
|
|||
|
3a62864a41
|
|||
|
109095e35e
|
|||
|
2dd6f39ac6
|
|||
|
b0f653e73b
|
|||
|
d552fb9220
|
|||
|
77339620e6
|
|||
|
846183bbb1
|
|||
|
1d53f6df7a
|
|||
|
58d13a4107
|
|||
|
f7d99d8d7b
|
|||
|
d9dd003b01
|
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: "publish"
|
||||
name: "publish desktop apps"
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "desktop-app-v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
.direnv
|
||||
!.env.example
|
||||
venv
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
export interface IconsConfig {
|
||||
codePoints: Record<string, string>
|
||||
inputPath: string
|
||||
outputPath: string
|
||||
icons: string[]
|
||||
}
|
||||
|
||||
const config: IconsConfig = {
|
||||
/** @type {import('./src/tools/icons-config').IconsConfig} */
|
||||
const config = {
|
||||
inputPath:
|
||||
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
||||
outputPath: "src/lib/assets/icons.min.woff2",
|
||||
icons: [
|
||||
"adjust",
|
||||
"add",
|
||||
"piano",
|
||||
"keyboard",
|
||||
@@ -26,6 +21,7 @@ const config: IconsConfig = {
|
||||
"cable",
|
||||
"person",
|
||||
"sync",
|
||||
"school",
|
||||
"restart_alt",
|
||||
"usb",
|
||||
"usb_off",
|
||||
@@ -90,6 +86,12 @@ const config: IconsConfig = {
|
||||
"timer",
|
||||
"target",
|
||||
"download",
|
||||
"download_2",
|
||||
"upload_2",
|
||||
"stat_minus_2",
|
||||
"stat_2",
|
||||
"description",
|
||||
"add_circle",
|
||||
],
|
||||
codePoints: {
|
||||
speed: "e9e4",
|
||||
@@ -104,6 +106,10 @@ const config: IconsConfig = {
|
||||
upload_file: "e9fc",
|
||||
no_sound: "e710",
|
||||
sentiment_extremely_dissatisfied: "f194",
|
||||
download_2: "f523",
|
||||
upload_2: "ff52",
|
||||
stat_minus_2: "e69c",
|
||||
stat_2: "e699",
|
||||
},
|
||||
}
|
||||
|
||||
177
package-lock.json
generated
177
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "charachorder-device-manager",
|
||||
"version": "0.6.5",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "charachorder-device-manager",
|
||||
"version": "0.6.5",
|
||||
"version": "1.2.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
@@ -15,8 +15,8 @@
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@codemirror/state": "^6.2.1",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.11",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.12",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.16",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.17",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
@@ -28,6 +28,7 @@
|
||||
"@types/dom-view-transitions": "^1.0.1",
|
||||
"@types/flexsearch": "^0.7.3",
|
||||
"@types/w3c-web-serial": "^1.0.3",
|
||||
"@types/w3c-web-usb": "^1.0.10",
|
||||
"@vite-pwa/sveltekit": "^0.2.7",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -52,12 +53,11 @@
|
||||
"svelte-check": "^3.5.1",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"tippy.js": "^6.3.7",
|
||||
"ts-node": "^10.9.1",
|
||||
"typesafe-i18n": "^5.26.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-mkcert": "^1.16.0",
|
||||
"vite-plugin-pwa": "^0.16.5",
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
"vitest": "^0.34.4"
|
||||
}
|
||||
},
|
||||
@@ -1894,28 +1894,6 @@
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz",
|
||||
@@ -2417,15 +2395,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/material-symbols-rounded": {
|
||||
"version": "5.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/material-symbols-rounded/-/material-symbols-rounded-5.0.11.tgz",
|
||||
"integrity": "sha512-WelrZz3MJErCcMPFPJBWS8mL2dY80lnS/eKYisiiUp9dW2rsU/yULQ/ihf4fBtPc5v9PA/1Uh7gW/X/Bll6CuQ==",
|
||||
"version": "5.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/material-symbols-rounded/-/material-symbols-rounded-5.0.16.tgz",
|
||||
"integrity": "sha512-HtH/bpUBj/9irIouf2uPaB+qf6HKpR0JFxSDK2HGaqOLsJqIxs4RJB2Y9IXASwTN50FBd1g8KZ6O5vNYEsU94A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@fontsource-variable/noto-sans-mono": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-mono/-/noto-sans-mono-5.0.12.tgz",
|
||||
"integrity": "sha512-OMDL6elwLMSEOdmWyRkA4ETGLyXv84LAtFPoZFj+N1pUy0L1om9Qz5f7DzwxdRA0HbciuJKRBa7XQGkMLjQZUg==",
|
||||
"version": "5.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-mono/-/noto-sans-mono-5.0.17.tgz",
|
||||
"integrity": "sha512-EpK1L28ZahAschdLmCCjHVoYNAystRlx/eduGKt9F6m4zln7x+CleAVWwqgAXOp/GDuTgVWwr1aPqcRFzwjQbg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
@@ -3228,30 +3206,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz",
|
||||
@@ -3348,6 +3302,12 @@
|
||||
"integrity": "sha512-R4J/OjqKAUFQoXVIkaUTfzb/sl6hLh/ZhDTfowJTRMa7LhgEmI/jXV4zsL1u8HpNa853BxwNmDIr0pauizzwSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/w3c-web-usb": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz",
|
||||
"integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
|
||||
@@ -3633,12 +3593,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -4519,12 +4473,6 @@
|
||||
"url": "https://github.com/sponsors/d-fischer"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
@@ -5093,15 +5041,6 @@
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-sequences": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
||||
@@ -5528,9 +5467,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -7629,12 +7568,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/map-obj": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
|
||||
@@ -10531,49 +10464,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
|
||||
@@ -10916,12 +10806,6 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@@ -11043,13 +10927,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "0.16.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.5.tgz",
|
||||
"integrity": "sha512-Ahol4dwhMP2UHPQXkllSlXbihOaDFnvBIDPmAxoSZ1EObBUJGP4CMRyCyAVkIHjd6/H+//vH0DM2ON+XxHr81g==",
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.4.tgz",
|
||||
"integrity": "sha512-j9iiyinFOYyof4Zk3Q+DtmYyDVBDAi6PuMGNGq6uGI0pw7E+LNm9e+nQ2ep9obMP/kjdWwzilqUrlfVRj9OobA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"workbox-build": "^7.0.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
@@ -11061,7 +10945,7 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^3.1.0 || ^4.0.0",
|
||||
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0",
|
||||
"workbox-build": "^7.0.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
}
|
||||
@@ -11843,15 +11727,6 @@
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
16
package.json
16
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "charachorder-device-manager",
|
||||
"version": "0.7.0",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CharaChorder/DeviceManager.git"
|
||||
},
|
||||
"homepage": "https://github.com/CharaChorder/DeviceManager",
|
||||
"homepage": "https://docs.charachorder.com",
|
||||
"bugs": {
|
||||
"url": "https://github.com/CharaChorder/DeviceManager/issues"
|
||||
},
|
||||
@@ -23,8 +23,8 @@
|
||||
"postinstall": "patch-package",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
|
||||
"version": "ts-node-esm src/tools/version.ts && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
|
||||
"minify-icons": "node src/tools/minify-icon-font.js",
|
||||
"version": "node src/tools/version.js && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
|
||||
"lint": "prettier --plugin-search-dir . --check .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"typesafe-i18n": "typesafe-i18n"
|
||||
@@ -35,8 +35,8 @@
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@codemirror/state": "^6.2.1",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.11",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.12",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.16",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.17",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
@@ -48,6 +48,7 @@
|
||||
"@types/dom-view-transitions": "^1.0.1",
|
||||
"@types/flexsearch": "^0.7.3",
|
||||
"@types/w3c-web-serial": "^1.0.3",
|
||||
"@types/w3c-web-usb": "^1.0.10",
|
||||
"@vite-pwa/sveltekit": "^0.2.7",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -72,12 +73,11 @@
|
||||
"svelte-check": "^3.5.1",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"tippy.js": "^6.3.7",
|
||||
"ts-node": "^10.9.1",
|
||||
"typesafe-i18n": "^5.26.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-mkcert": "^1.16.0",
|
||||
"vite-plugin-pwa": "^0.16.5",
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
"vitest": "^0.34.4"
|
||||
},
|
||||
"type": "module"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.6.5"
|
||||
version = "1.2.0"
|
||||
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": "0.6.5" },
|
||||
"package": { "productName": "amacc1ng", "version": "1.2.0" },
|
||||
"tauri": {
|
||||
"allowlist": { "all": false },
|
||||
"bundle": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" href="%sveltekit.assets%/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
@@ -10,6 +10,8 @@ interface ImportMetaEnv {
|
||||
|
||||
readonly VITE_HOMEPAGE_URL: string
|
||||
readonly VITE_BUGS_URL: string
|
||||
readonly VITE_DOCS_URL: string
|
||||
readonly VIET_LEARN_URL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -8,6 +8,9 @@ const de = {
|
||||
REDO: "Wiederholen",
|
||||
SAVE: "Speichern",
|
||||
},
|
||||
update: {
|
||||
TITLE: "Gerät aktualisieren",
|
||||
},
|
||||
sync: {
|
||||
TITLE_READ: "Neueste Änderungen werden abgerufen",
|
||||
TITLE_WRITE: "Änderungen werden gespeichert",
|
||||
@@ -26,10 +29,14 @@ const de = {
|
||||
actionSearch: {
|
||||
PLACEHOLDER: "Nach Aktionen suchen",
|
||||
CURRENT_ACTION: "Aktuelle Aktion",
|
||||
NEXT_ACTION: "Aktion nach dem nächsten Speichern",
|
||||
DELETE: "Entfernen",
|
||||
filter: {
|
||||
ALL: "Alle",
|
||||
},
|
||||
LIVE_LAYOUT_INFO: "Diese Aktion wurde auf Basis des Systemtastaturlayouts ermittelt.",
|
||||
SHIFT_WARNING: "Diese Aktion hält <kbd class='icon'>shift</kbd>",
|
||||
ALT_CODE_WARNING: "Dieses Alt-Code Makro funktioniert nur unter Windows",
|
||||
},
|
||||
share: {
|
||||
TITLE: "Teilen",
|
||||
@@ -56,10 +63,14 @@ const de = {
|
||||
DISCONNECT: "Entfernen",
|
||||
TERMINAL: "Konsole",
|
||||
APPLY_SETTINGS: "Änderungen auf das Gerät brennen",
|
||||
NO_DEVICE: "Kein Gerät verbunden",
|
||||
LINUX_PERMISSIONS:
|
||||
"Auf den meisten Linux-basierten Systemen müssen zuerst Berechtigungen angepasst werden.",
|
||||
bootMenu: {
|
||||
TITLE: "Bootmenü",
|
||||
REBOOT: "Neustarten",
|
||||
BOOTLOADER: "Bootloader",
|
||||
POWER_WARNING: "Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.",
|
||||
},
|
||||
},
|
||||
browserWarning: {
|
||||
@@ -72,7 +83,8 @@ const de = {
|
||||
"Auch wenn alle Chromium-basieren Desktop Browser diese Voraussetzung grundsätzlich erfüllen, haben einige Browser ",
|
||||
INFO_BROWSER_INFIX: "wie zum Beispiel Brave",
|
||||
INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.",
|
||||
DOWNLOAD_APP: "Desktop-app herunterladen",
|
||||
DOWNLOAD_APP:
|
||||
"Chrome oder Edge werden offiziell unterstützt, andere Browser könnten aber auch funktionieren.",
|
||||
},
|
||||
changes: {
|
||||
TITLE: "Änderungen importieren",
|
||||
@@ -96,8 +108,10 @@ const de = {
|
||||
TITLE: "Akkorde",
|
||||
HOLD_KEYS: "Akkord halten",
|
||||
NEW_CHORD: "Neuer Akkord",
|
||||
DUPLICATE: "Akkord existiert bereits",
|
||||
search: {
|
||||
PLACEHOLDER: "{0} Akkord{{|e}} durchsuchen",
|
||||
NO_RESULTS: "Keine Ergebnisse",
|
||||
},
|
||||
conflict: {
|
||||
TITLE: "Akkordkonflikt",
|
||||
@@ -106,6 +120,7 @@ const de = {
|
||||
CONFIRM: "Überschreiben",
|
||||
ABORT: "Überspringen",
|
||||
},
|
||||
TRY_TYPING: "Versuche hier zu tippen",
|
||||
},
|
||||
layout: {
|
||||
TITLE: "Layout",
|
||||
|
||||
@@ -8,6 +8,9 @@ const en = {
|
||||
REDO: "Redo",
|
||||
SAVE: "Save",
|
||||
},
|
||||
update: {
|
||||
TITLE: "Update your device",
|
||||
},
|
||||
backup: {
|
||||
TITLE: "Store History",
|
||||
INDIVIDUAL: "Individual backups",
|
||||
@@ -25,10 +28,14 @@ const en = {
|
||||
actionSearch: {
|
||||
PLACEHOLDER: "Search for actions",
|
||||
CURRENT_ACTION: "Current action",
|
||||
NEXT_ACTION: "Action after next save",
|
||||
DELETE: "Remove",
|
||||
filter: {
|
||||
ALL: "All",
|
||||
},
|
||||
LIVE_LAYOUT_INFO: "This output was determined using on your system layout.",
|
||||
SHIFT_WARNING: "This action holds <kbd class='icon'>shift</kbd>",
|
||||
ALT_CODE_WARNING: "This alt-code macro only works on Windows",
|
||||
},
|
||||
share: {
|
||||
TITLE: "Share",
|
||||
@@ -55,10 +62,13 @@ const en = {
|
||||
DISCONNECT: "Disconnect",
|
||||
TERMINAL: "Terminal",
|
||||
APPLY_SETTINGS: "Flash changes to device",
|
||||
NO_DEVICE: "No device connected",
|
||||
LINUX_PERMISSIONS: "Most Linux based systems need adjusted permissions in order to connect your device.",
|
||||
bootMenu: {
|
||||
TITLE: "Boot Menu",
|
||||
REBOOT: "Reboot",
|
||||
BOOTLOADER: "Bootloader",
|
||||
POWER_WARNING: "To reboot from bootloader you need to physically reconnect your device.",
|
||||
},
|
||||
},
|
||||
browserWarning: {
|
||||
@@ -70,7 +80,7 @@ const en = {
|
||||
"Though all chromium-based desktop browsers fulfill this requirement, some derivations such as Brave ",
|
||||
INFO_BROWSER_INFIX: "have been known to disable the API intentionally",
|
||||
INFO_BROWSER_SUFFIX: ".",
|
||||
DOWNLOAD_APP: "Download the desktop app",
|
||||
DOWNLOAD_APP: "Chrome or Edge are officially supported, but other browsers might work as well.",
|
||||
},
|
||||
changes: {
|
||||
TITLE: "Import changes",
|
||||
@@ -94,8 +104,10 @@ const en = {
|
||||
TITLE: "Chords",
|
||||
HOLD_KEYS: "Hold chord",
|
||||
NEW_CHORD: "New chord",
|
||||
DUPLICATE: "Chord already exists",
|
||||
search: {
|
||||
PLACEHOLDER: "Search {0} chord{{|s}}",
|
||||
NO_RESULTS: "No results",
|
||||
},
|
||||
conflict: {
|
||||
TITLE: "Chord conflict",
|
||||
@@ -104,6 +116,7 @@ const en = {
|
||||
CONFIRM: "Overwrite",
|
||||
ABORT: "Skip",
|
||||
},
|
||||
TRY_TYPING: "Try typing here",
|
||||
},
|
||||
layout: {
|
||||
TITLE: "Layout",
|
||||
|
||||
141
src/lib/assets/keymaps/ascii-macros.yml
Normal file
141
src/lib/assets/keymaps/ascii-macros.yml
Normal file
@@ -0,0 +1,141 @@
|
||||
name: ASCII Macros
|
||||
description: ASCII Characters that are macros for SHFT + key
|
||||
actions:
|
||||
33:
|
||||
id: "!"
|
||||
title: Exclamation Point
|
||||
34:
|
||||
id: '"'
|
||||
title: Double Quote
|
||||
35:
|
||||
id: "#"
|
||||
title: Hash Symbol
|
||||
36:
|
||||
id: "$"
|
||||
title: Dollar Sign
|
||||
37:
|
||||
id: "%"
|
||||
title: Percent
|
||||
38:
|
||||
id: "&"
|
||||
title: Ampersand
|
||||
40:
|
||||
id: "("
|
||||
title: Opening Parenthesis
|
||||
41:
|
||||
id: ")"
|
||||
title: Closing Parenthesis
|
||||
42:
|
||||
id: "*"
|
||||
title: Asterisk
|
||||
58:
|
||||
id: ":"
|
||||
title: Colon
|
||||
60:
|
||||
id: "<"
|
||||
title: Less Than
|
||||
62:
|
||||
id: ">"
|
||||
title: Greater Than
|
||||
63:
|
||||
id: "?"
|
||||
title: Question Mark
|
||||
64:
|
||||
id: "@"
|
||||
title: At Symbol
|
||||
65:
|
||||
id: "A"
|
||||
title: Uppercase A
|
||||
66:
|
||||
id: "B"
|
||||
title: Uppercase B
|
||||
67:
|
||||
id: "C"
|
||||
title: Uppercase C
|
||||
68:
|
||||
id: "D"
|
||||
title: Uppercase D
|
||||
69:
|
||||
id: "E"
|
||||
title: Uppercase E
|
||||
70:
|
||||
id: "F"
|
||||
title: Uppercase F
|
||||
71:
|
||||
id: "G"
|
||||
title: Uppercase G
|
||||
72:
|
||||
id: "H"
|
||||
title: Uppercase H
|
||||
73:
|
||||
id: "I"
|
||||
title: Uppercase I
|
||||
74:
|
||||
id: "J"
|
||||
title: Uppercase J
|
||||
75:
|
||||
id: "K"
|
||||
title: Uppercase K
|
||||
76:
|
||||
id: "L"
|
||||
title: Uppercase L
|
||||
77:
|
||||
id: "M"
|
||||
title: Uppercase M
|
||||
78:
|
||||
id: "N"
|
||||
title: Uppercase N
|
||||
79:
|
||||
id: "O"
|
||||
title: Uppercase O
|
||||
80:
|
||||
id: "P"
|
||||
title: Uppercase P
|
||||
81:
|
||||
id: "Q"
|
||||
title: Uppercase Q
|
||||
82:
|
||||
id: "R"
|
||||
title: Uppercase R
|
||||
83:
|
||||
id: "S"
|
||||
title: Uppercase S
|
||||
84:
|
||||
id: "T"
|
||||
title: Uppercase T
|
||||
85:
|
||||
id: "U"
|
||||
title: Uppercase U
|
||||
86:
|
||||
id: "V"
|
||||
title: Uppercase V
|
||||
87:
|
||||
id: "W"
|
||||
title: Uppercase W
|
||||
88:
|
||||
id: "X"
|
||||
title: Uppercase X
|
||||
89:
|
||||
id: "Y"
|
||||
title: Uppercase Y
|
||||
90:
|
||||
id: "Z"
|
||||
title: Uppercase Z
|
||||
94:
|
||||
id: "^"
|
||||
title: Caret
|
||||
95:
|
||||
id: "_"
|
||||
title: Underscore
|
||||
123:
|
||||
id: "{"
|
||||
title: Left Curly Brace
|
||||
124:
|
||||
id: "|"
|
||||
title: Pipe
|
||||
125:
|
||||
id: "}"
|
||||
title: Right Curly Brace
|
||||
126:
|
||||
id: "~"
|
||||
title: Tilde
|
||||
@@ -7,39 +7,9 @@ actions:
|
||||
description: |
|
||||
While SPACE is used for keymaps and chord, just a " " is used in chord outputs.
|
||||
This action is unique in this way. Technically it is "printable", but it is not visible.
|
||||
33:
|
||||
id: "!"
|
||||
title: Exclamation Point
|
||||
34:
|
||||
id: '"'
|
||||
title: Double Quote
|
||||
35:
|
||||
id: "#"
|
||||
title: Hash Symbol
|
||||
36:
|
||||
id: "$"
|
||||
title: Dollar Sign
|
||||
37:
|
||||
id: "%"
|
||||
title: Percent
|
||||
38:
|
||||
id: "&"
|
||||
title: Ampersand
|
||||
39:
|
||||
id: "'"
|
||||
title: Single Quote
|
||||
40:
|
||||
id: "("
|
||||
title: Opening Parenthesis
|
||||
41:
|
||||
id: ")"
|
||||
title: Closing Parenthesis
|
||||
42:
|
||||
id: "*"
|
||||
title: Asterisk
|
||||
43:
|
||||
id: "+"
|
||||
title: Plus
|
||||
44:
|
||||
id: ","
|
||||
title: Comma
|
||||
@@ -82,105 +52,12 @@ actions:
|
||||
57:
|
||||
id: "9"
|
||||
title: Nine
|
||||
58:
|
||||
id: ":"
|
||||
title: Colon
|
||||
59:
|
||||
id: ";"
|
||||
title: Semicolon
|
||||
60:
|
||||
id: "<"
|
||||
title: Less Than
|
||||
61:
|
||||
id: "="
|
||||
title: Equals
|
||||
62:
|
||||
id: ">"
|
||||
title: Greater Than
|
||||
63:
|
||||
id: "?"
|
||||
title: Question Mark
|
||||
64:
|
||||
id: "@"
|
||||
title: At Symbol
|
||||
65:
|
||||
id: "A"
|
||||
title: Uppercase A
|
||||
66:
|
||||
id: "B"
|
||||
title: Uppercase B
|
||||
67:
|
||||
id: "C"
|
||||
title: Uppercase C
|
||||
68:
|
||||
id: "D"
|
||||
title: Uppercase D
|
||||
69:
|
||||
id: "E"
|
||||
title: Uppercase E
|
||||
70:
|
||||
id: "F"
|
||||
title: Uppercase F
|
||||
71:
|
||||
id: "G"
|
||||
title: Uppercase G
|
||||
72:
|
||||
id: "H"
|
||||
title: Uppercase H
|
||||
73:
|
||||
id: "I"
|
||||
title: Uppercase I
|
||||
74:
|
||||
id: "J"
|
||||
title: Uppercase J
|
||||
75:
|
||||
id: "K"
|
||||
title: Uppercase K
|
||||
76:
|
||||
id: "L"
|
||||
title: Uppercase L
|
||||
77:
|
||||
id: "M"
|
||||
title: Uppercase M
|
||||
78:
|
||||
id: "N"
|
||||
title: Uppercase N
|
||||
79:
|
||||
id: "O"
|
||||
title: Uppercase O
|
||||
80:
|
||||
id: "P"
|
||||
title: Uppercase P
|
||||
81:
|
||||
id: "Q"
|
||||
title: Uppercase Q
|
||||
82:
|
||||
id: "R"
|
||||
title: Uppercase R
|
||||
83:
|
||||
id: "S"
|
||||
title: Uppercase S
|
||||
84:
|
||||
id: "T"
|
||||
title: Uppercase T
|
||||
85:
|
||||
id: "U"
|
||||
title: Uppercase U
|
||||
86:
|
||||
id: "V"
|
||||
title: Uppercase V
|
||||
87:
|
||||
id: "W"
|
||||
title: Uppercase W
|
||||
88:
|
||||
id: "X"
|
||||
title: Uppercase X
|
||||
89:
|
||||
id: "Y"
|
||||
title: Uppercase Y
|
||||
90:
|
||||
id: "Z"
|
||||
title: Uppercase Z
|
||||
91:
|
||||
id: "["
|
||||
title: Left Bracket
|
||||
@@ -190,12 +67,6 @@ actions:
|
||||
93:
|
||||
id: "]"
|
||||
title: Right Bracket
|
||||
94:
|
||||
id: "^"
|
||||
title: Caret
|
||||
95:
|
||||
id: "_"
|
||||
title: Underscore
|
||||
96:
|
||||
id: "`"
|
||||
title: Backtick
|
||||
@@ -277,19 +148,6 @@ actions:
|
||||
122:
|
||||
id: "z"
|
||||
title: Lowercase z
|
||||
123:
|
||||
id: "{"
|
||||
title: Left Curly Brace
|
||||
124:
|
||||
id: "|"
|
||||
title: Pipe
|
||||
125:
|
||||
id: "}"
|
||||
title: Right Curly Brace
|
||||
126:
|
||||
id: "~"
|
||||
title: Tilde
|
||||
127:
|
||||
id: "DEL"
|
||||
title: Delete
|
||||
icon: delete_forever
|
||||
|
||||
@@ -6,34 +6,73 @@ type: unassigned
|
||||
actions:
|
||||
600:
|
||||
id: "LH_THUMB_3_3D"
|
||||
title: Left Hand Thumb Top 3D Click
|
||||
title: "Left Hand Thumb Bottom 3D Click"
|
||||
icon: "adjust"
|
||||
601:
|
||||
id: "LH_THUMB_2_3D"
|
||||
title: Left Hand Thumb Middle 3D Click
|
||||
title: "Left Hand Thumb Middle 3D Click"
|
||||
icon: "adjust"
|
||||
602:
|
||||
id: "LH_THUMB_1_3D"
|
||||
title: Left Hand Thumb Bottom 3D Click
|
||||
title: "Left Hand Thumb Top 3D Click"
|
||||
icon: "adjust"
|
||||
603:
|
||||
id: "LH_INDEX_3D"
|
||||
title: Left Hand Index Finger 3D Click
|
||||
title: "Left Hand Index Finger 3D Click"
|
||||
icon: "adjust"
|
||||
604:
|
||||
id: "LH_MID_1_3D"
|
||||
title: Left Hand Middle Finger 3D Click
|
||||
title: "Left Hand Middle Finger 3D Click"
|
||||
icon: "adjust"
|
||||
605:
|
||||
id: "LH_RING_1_3D"
|
||||
title: Left Hand Ring Finger 3D Click
|
||||
title: "Left Hand Ring Finger 3D Click"
|
||||
icon: "adjust"
|
||||
606:
|
||||
id: "LH_PINKY_3D"
|
||||
title: Left Hand Pinky 3D Click,
|
||||
# TODO...
|
||||
# ["607", "CharaChorder One", "LH_MID_2_3D", "", ""],
|
||||
# ["608", "CharaChorder One", "LH_RING_2_3D", "", ""],
|
||||
# ["609", "CharaChorder One", "RH_THUMB_3_3D", "", ""],
|
||||
# ["610", "CharaChorder One", "RH_THUMB_2_3D", "", ""],
|
||||
# ["611", "CharaChorder One", "RH_THUMB_1_3D", "", ""],
|
||||
# ["612", "CharaChorder One", "RH_INDEX_3D", "", ""],
|
||||
# ["613", "CharaChorder One", "RH_MID_1_3D", "", ""],
|
||||
# ["614", "CharaChorder One", "RH_RING_1_3D", "", ""],
|
||||
# ["615", "CharaChorder One", "RH_PINKY_3D", "", ""],
|
||||
# ["616", "CharaChorder One", "RH_MID_2_3D", "", ""],
|
||||
# ["617", "CharaChorder One", "RH_RING_2_3D", "", ""]
|
||||
title: "Left Hand Pinky 3D Click"
|
||||
icon: "adjust"
|
||||
607:
|
||||
id: "LH_MID_2_3D"
|
||||
title: "Left Hand Middle Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
608:
|
||||
id: "LH_RING_2_3D"
|
||||
title: "Left Hand Ring Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
609:
|
||||
id: "RH_THUMB_3_3D"
|
||||
title: "Right Hand Thumb Bottom 3D Click"
|
||||
icon: "adjust"
|
||||
610:
|
||||
id: "RH_THUMB_2_3D"
|
||||
title: "Right Hand Thumb Middle 3D Click"
|
||||
icon: "adjust"
|
||||
611:
|
||||
id: "RH_THUMB_1_3D"
|
||||
title: "Right Hand Thumb Top 3D Click"
|
||||
icon: "adjust"
|
||||
612:
|
||||
id: "RH_INDEX_3D"
|
||||
title: "Right Hand Index Finger 3D Click"
|
||||
icon: "adjust"
|
||||
613:
|
||||
id: "RH_MID_1_3D"
|
||||
title: "Right Hand Middle Finger 3D Click"
|
||||
icon: "adjust"
|
||||
614:
|
||||
id: "RH_RING_1_3D"
|
||||
title: "Right Hand Ring Finger 3D Click"
|
||||
icon: "adjust"
|
||||
615:
|
||||
id: "RH_PINKY_3D"
|
||||
title: "Right Hand Pinky 3D Click"
|
||||
icon: "adjust"
|
||||
616:
|
||||
id: "RH_MID_2_3D"
|
||||
title: "Right Hand Middle Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
617:
|
||||
id: "RH_RING_2_3D"
|
||||
title: "Right Hand Ring Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
|
||||
@@ -26,7 +26,7 @@ actions:
|
||||
536:
|
||||
id: "DUP"
|
||||
title: Repeat Last Note
|
||||
icon: control_point_duplicate
|
||||
icon: copy_all
|
||||
description: |
|
||||
In character entry, it repeats your last input.
|
||||
In chorded entry, it is used for words with repeating letters.
|
||||
|
||||
@@ -4,9 +4,9 @@ icon: keyboard
|
||||
actions:
|
||||
512: &left_ctrl
|
||||
id: "LEFT_CTRL"
|
||||
display: CTRL
|
||||
title: Control Keyboard Modifier
|
||||
variant: left
|
||||
icon: keyboard_control_key
|
||||
513: &left_shift
|
||||
id: "LEFT_SHIFT"
|
||||
title: Shift Keyboard Modifier
|
||||
@@ -14,14 +14,14 @@ actions:
|
||||
icon: shift
|
||||
514: &left_alt
|
||||
id: "LEFT_ALT"
|
||||
display: ALT
|
||||
title: Alt Keyboard Modifier
|
||||
variant: left
|
||||
icon: keyboard_option_key
|
||||
515: &left_gui
|
||||
id: "LEFT_GUI"
|
||||
title: GUI Keyboard Modifier
|
||||
icon: apps
|
||||
variant: left
|
||||
icon: keyboard_command_key
|
||||
516:
|
||||
variationOf: 512
|
||||
<<: *left_ctrl
|
||||
@@ -31,14 +31,17 @@ actions:
|
||||
variationOf: 513
|
||||
<<: *left_shift
|
||||
id: "RIGHT_SHIFT"
|
||||
variant: right
|
||||
518:
|
||||
variationOf: 514
|
||||
<<: *left_alt
|
||||
id: "RIGHT_ALT"
|
||||
variant: right
|
||||
519:
|
||||
variationOf: 515
|
||||
<<: *left_gui
|
||||
id: "RIGHT_GUI"
|
||||
variant: right
|
||||
520:
|
||||
id: "RELEASE_MOD"
|
||||
title: Release all keyboard modifiers
|
||||
@@ -51,3 +54,11 @@ actions:
|
||||
id: "RELEASE_KEYS"
|
||||
title: Release all keys, but not keyboard modifiers
|
||||
icon: text_rotate_up
|
||||
523:
|
||||
id: "PRESS_NEXT"
|
||||
title: "Press and do not release the next key/action"
|
||||
icon: download
|
||||
524:
|
||||
id: "RELEASE_NEXT"
|
||||
title: "Release the next key/action in the sequence"
|
||||
icon: upload
|
||||
|
||||
3
src/lib/assets/keymaps/keymap.d.ts
vendored
3
src/lib/assets/keymaps/keymap.d.ts
vendored
@@ -2,6 +2,7 @@ export interface KeymapCategory {
|
||||
name: string
|
||||
description: string
|
||||
icon?: string
|
||||
display?: string
|
||||
type?: "unassigned"
|
||||
actions: Record<number, Partial<ActionInfo>>
|
||||
}
|
||||
@@ -10,7 +11,9 @@ export interface ActionInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
display: string
|
||||
description: string
|
||||
variant: "left" | "right"
|
||||
variantOf: number
|
||||
keyCode: string
|
||||
}
|
||||
|
||||
@@ -1,83 +1,30 @@
|
||||
name: Lite
|
||||
name: 103-key
|
||||
col:
|
||||
- row:
|
||||
- key: 110
|
||||
- key: 112
|
||||
offset: [ 1, 0 ]
|
||||
- key: 113
|
||||
- key: 114
|
||||
- key: 115
|
||||
- key: 116
|
||||
offset: [ 0.5, 0 ]
|
||||
- key: 117
|
||||
- key: 118
|
||||
- key: 119
|
||||
- key: 120
|
||||
offset: [ 0.5, 0 ]
|
||||
- key: 121
|
||||
- key: 122
|
||||
- key: 123
|
||||
- key: 124
|
||||
offset: [ 0.25, 0 ]
|
||||
- key: 125
|
||||
- key: 126
|
||||
- offset: [ 0, 0.25 ]
|
||||
row:
|
||||
- key: 1
|
||||
- key: 2
|
||||
- key: 3
|
||||
- key: 4
|
||||
- key: 5
|
||||
- key: 6
|
||||
- key: 7
|
||||
- key: 8
|
||||
- key: 9
|
||||
- key: 10
|
||||
- key: 11
|
||||
- key: 12
|
||||
- key: 13
|
||||
- key: 15
|
||||
size: [ 2, 1 ]
|
||||
- key: 75
|
||||
offset: [ 0.25, 0 ]
|
||||
- key: 80
|
||||
- key: 85
|
||||
- key: 90
|
||||
offset: [ 0.25, 0 ]
|
||||
- key: 95
|
||||
- key: 100
|
||||
- key: 105
|
||||
- row:
|
||||
- key: 16
|
||||
size: [ 1.5, 1 ]
|
||||
- key: 17
|
||||
- key: 18
|
||||
- key: 19
|
||||
- key: 20
|
||||
- key: 21
|
||||
- key: 22
|
||||
- key: 23
|
||||
- key: 24
|
||||
- key: 25
|
||||
- key: 26
|
||||
- key: 27
|
||||
- key: 28
|
||||
- key: 29
|
||||
size: [ 1.5, 1 ]
|
||||
- key: 76
|
||||
offset: [ 0.25, 0 ]
|
||||
- key: 81
|
||||
- key: 86
|
||||
- key: 91
|
||||
offset: [ 0.25, 0 ]
|
||||
- key: 96
|
||||
- key: 101
|
||||
- key: 106
|
||||
size: [ 1, 2 ]
|
||||
- offset: [ 0, -1 ]
|
||||
- key: 41
|
||||
- key: 58
|
||||
offset: [1, 0]
|
||||
- key: 59
|
||||
- key: 60
|
||||
- key: 61
|
||||
- key: 62
|
||||
offset: [0.5, 0]
|
||||
- key: 63
|
||||
- key: 64
|
||||
- key: 65
|
||||
- key: 66
|
||||
offset: [0.5, 0]
|
||||
- key: 67
|
||||
- key: 68
|
||||
- key: 69
|
||||
- key: 70
|
||||
offset: [0.25, 0]
|
||||
- key: 71
|
||||
- key: 72
|
||||
- offset: [0, 0.25]
|
||||
row:
|
||||
- key: 53
|
||||
- key: 30
|
||||
size: [ 2, 1 ]
|
||||
- key: 31
|
||||
- key: 32
|
||||
- key: 33
|
||||
@@ -87,57 +34,109 @@ col:
|
||||
- key: 37
|
||||
- key: 38
|
||||
- key: 39
|
||||
- key: 40
|
||||
- key: 41
|
||||
- key: 43
|
||||
size: [ 2, 1 ]
|
||||
- key: 92
|
||||
offset: [ 3.5, 0 ]
|
||||
- key: 97
|
||||
- key: 102
|
||||
- row:
|
||||
- key: 44
|
||||
size: [ 2.5, 1 ]
|
||||
- key: 45
|
||||
- key: 46
|
||||
- key: 42
|
||||
size: [2, 1]
|
||||
- key: 73
|
||||
offset: [0.25, 0]
|
||||
- key: 74
|
||||
- key: 75
|
||||
- key: 83
|
||||
offset: [0.25, 0]
|
||||
- key: 84
|
||||
- key: 85
|
||||
- key: 86
|
||||
- row:
|
||||
- key: 43
|
||||
size: [1.5, 1]
|
||||
- key: 20
|
||||
- key: 26
|
||||
- key: 8
|
||||
- key: 21
|
||||
- key: 23
|
||||
- key: 28
|
||||
- key: 24
|
||||
- key: 12
|
||||
- key: 18
|
||||
- key: 19
|
||||
- key: 47
|
||||
- key: 48
|
||||
- key: 49
|
||||
- key: 50
|
||||
- key: 40
|
||||
size: [1.5, 1]
|
||||
- key: 76
|
||||
offset: [0.25, 0]
|
||||
- key: 77
|
||||
- key: 78
|
||||
- key: 95
|
||||
offset: [0.25, 0]
|
||||
- key: 96
|
||||
- key: 97
|
||||
- key: 87
|
||||
size: [1, 2]
|
||||
- offset: [0, -1]
|
||||
row:
|
||||
- key: 57
|
||||
size: [2, 1]
|
||||
- key: 4
|
||||
- key: 22
|
||||
- key: 7
|
||||
- key: 9
|
||||
- key: 10
|
||||
- key: 11
|
||||
- key: 13
|
||||
- key: 14
|
||||
- key: 15
|
||||
- key: 51
|
||||
- key: 52
|
||||
- key: 53
|
||||
- key: 49
|
||||
size: [2, 1]
|
||||
- key: 92
|
||||
offset: [3.5, 0]
|
||||
- key: 93
|
||||
- key: 94
|
||||
- row:
|
||||
- key: 225
|
||||
size: [2.5, 1]
|
||||
- key: 29
|
||||
- key: 27
|
||||
- key: 6
|
||||
- key: 25
|
||||
- key: 5
|
||||
- key: 17
|
||||
- key: 16
|
||||
- key: 54
|
||||
- key: 55
|
||||
- key: 57
|
||||
size: [ 2.5, 1 ]
|
||||
- key: 83
|
||||
offset: [ 1.25, 0 ]
|
||||
- key: 93
|
||||
offset: [ 1.25, 0 ]
|
||||
- key: 98
|
||||
- key: 103
|
||||
- key: 108
|
||||
size: [ 1, 2 ]
|
||||
- offset: [ 0, -1 ]
|
||||
row:
|
||||
- key: 58
|
||||
size: [ 1.5, 1 ]
|
||||
- key: 59
|
||||
- key: 60
|
||||
size: [ 1.5, 1 ]
|
||||
- key: 61
|
||||
size: [ 7, 1 ]
|
||||
- key: 62
|
||||
size: [ 1.5, 1 ]
|
||||
- key: 63
|
||||
- key: 64
|
||||
size: [ 1.5, 1 ]
|
||||
- key: 79
|
||||
offset: [ 0.25, 0 ]
|
||||
- key: 84
|
||||
- key: 56
|
||||
- key: 229
|
||||
size: [2.5, 1]
|
||||
- key: 82
|
||||
offset: [1.25, 0]
|
||||
- key: 89
|
||||
offset: [1.25, 0]
|
||||
- key: 90
|
||||
- key: 91
|
||||
- key: 88
|
||||
size: [1, 2]
|
||||
- offset: [0, -1]
|
||||
row:
|
||||
- key: 224
|
||||
size: [1.5, 1]
|
||||
- key: 227
|
||||
- key: 226
|
||||
size: [1.5, 1]
|
||||
- key: 44
|
||||
size: [7, 1]
|
||||
- key: 230
|
||||
size: [1.5, 1]
|
||||
- key: 231
|
||||
- key: 228
|
||||
size: [1.5, 1]
|
||||
- key: 80
|
||||
offset: [0.25, 0]
|
||||
- key: 81
|
||||
- key: 79
|
||||
- key: 98
|
||||
offset: [0.25, 0]
|
||||
size: [2, 1]
|
||||
- key: 99
|
||||
offset: [ 0.25, 0 ]
|
||||
size: [ 2, 1 ]
|
||||
- key: 104
|
||||
|
||||
|
||||
@@ -1,118 +1,118 @@
|
||||
settings:
|
||||
1:
|
||||
0x1:
|
||||
title: Enable Serial Header
|
||||
description: boolean 0 or 1, default is 0
|
||||
2:
|
||||
0x2:
|
||||
title: Enable Serial Logging
|
||||
description: boolean 0 or 1, default is 0
|
||||
3:
|
||||
0x3:
|
||||
title: Enable Serial Debugging
|
||||
description: boolean 0 or 1, default is 0
|
||||
4:
|
||||
0x4:
|
||||
title: Enable Serial Raw
|
||||
description: boolean 0 or 1, default is 0
|
||||
5:
|
||||
0x5:
|
||||
title: Enable Serial Chord
|
||||
description: boolean 0 or 1, default is 0
|
||||
6:
|
||||
0x6:
|
||||
title: Enable Serial Keyboard
|
||||
description: boolean 0 or 1, default is 0
|
||||
7:
|
||||
0x7:
|
||||
title: Enable Serial Mouse
|
||||
description: boolean 0 or 1, default is 0
|
||||
11:
|
||||
0x11:
|
||||
title: Enable USB HID Keyboard
|
||||
description: boolean 0 or 1, default is 1
|
||||
12:
|
||||
0x12:
|
||||
title: Enable Character Entry
|
||||
description: boolean 0 or 1
|
||||
13:
|
||||
0x13:
|
||||
title: GUI-CTRL Swap Mode
|
||||
description: boolean 0 or 1; 1 swaps keymap 0 and 1. (CCL only)
|
||||
14:
|
||||
0x14:
|
||||
title: Key Scan Duration
|
||||
description: scan rate described in milliseconds; default is 2ms = 500Hz
|
||||
15:
|
||||
0x15:
|
||||
title: Key Debounce Press Duration
|
||||
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
||||
16:
|
||||
0x16:
|
||||
title: Key Debounce Release Duration
|
||||
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
||||
17:
|
||||
0x17:
|
||||
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:
|
||||
0x21:
|
||||
title: Enable USB HID Mouse
|
||||
description: boolean 0 or 1; default is 1
|
||||
22:
|
||||
0x22:
|
||||
title: Slow Mouse Speed
|
||||
description: pixels to move at the mouse poll rate; default for CC1 is 5 = 250px/s
|
||||
23:
|
||||
0x23:
|
||||
title: Fast Mouse Speed
|
||||
description: pixels to move at the mouse poll rate; default for CC1 is 25 = 1250px/s
|
||||
24:
|
||||
0x24:
|
||||
title: Enable Active Mouse
|
||||
description: boolean 0 or 1; moves mouse back and forth every 60s
|
||||
25:
|
||||
0x25:
|
||||
title: Mouse Scroll Speed
|
||||
description: default is 1; polls at 1/4th the rate of the mouse move updates
|
||||
26:
|
||||
0x26:
|
||||
title: Mouse Poll Duration
|
||||
description: poll rate described in milliseconds; default is 20ms = 50Hz
|
||||
31:
|
||||
0x31:
|
||||
title: Enable Chording
|
||||
description: boolean 0 or 1
|
||||
32:
|
||||
0x32:
|
||||
title: Enable Chording Character Counter Timeout
|
||||
description: boolean 0 or 1; default is 1
|
||||
33:
|
||||
0x33:
|
||||
title: Chording Character Counter Timeout Timer
|
||||
description: 0-255 deciseconds; default is 40 or 4.0 seconds
|
||||
34:
|
||||
0x34:
|
||||
title: Chord Detection Press Tolerance(ms)
|
||||
description: 1-50 milliseconds
|
||||
35:
|
||||
0x35:
|
||||
title: Chord Detection Release Tolerance(ms)
|
||||
description: 1-50 milliseconds
|
||||
41:
|
||||
0x41:
|
||||
title: Enable Spurring
|
||||
description: boolean 0 or 1; default is 1
|
||||
42:
|
||||
0x42:
|
||||
title: Enable Spurring Character Counter Timeout
|
||||
description: boolean 0 or 1; default is 1
|
||||
43:
|
||||
0x43:
|
||||
title: Spurring Character Counter Timeout Timer
|
||||
description: 0-255 seconds; default is 240
|
||||
51:
|
||||
0x51:
|
||||
title: Enable Arpeggiates
|
||||
description: boolean 0 or 1; default is 1
|
||||
54:
|
||||
0x54:
|
||||
title: Arpeggiate Tolerance
|
||||
description: in milliseconds; default 800ms
|
||||
61:
|
||||
0x61:
|
||||
title: Enable Compound Chording (coming soon)
|
||||
description: boolean 0 or 1; default is 0
|
||||
64:
|
||||
0x64:
|
||||
title: Compound Tolerance
|
||||
description: in milliseconds; default 1500ms
|
||||
81:
|
||||
0x81:
|
||||
title: LED Brightness
|
||||
description: 0-50 (CCL only); default is 5, which draws around 100 mA of current
|
||||
82:
|
||||
0x82:
|
||||
title: LED Color Code
|
||||
description: Color Codes to be listed (CCL only)
|
||||
83:
|
||||
0x83:
|
||||
title: Enable LED Key Highlight (coming soon)
|
||||
description: boolean 0 or 1 (CCL only)
|
||||
84:
|
||||
0x84:
|
||||
title: Enable LEDs
|
||||
description: boolean 0 or 1; default is 1 (CCL only)
|
||||
91:
|
||||
0x91:
|
||||
title: Operating System
|
||||
description: Operating system codes listed below
|
||||
92:
|
||||
0x92:
|
||||
title: Enable Realtime Feedback
|
||||
description: boolean 0 or 1; default is 1
|
||||
93:
|
||||
0x93:
|
||||
title: Enable CharaChorder Ready on startup
|
||||
description: boolean 0 or 1; default is 1
|
||||
|
||||
@@ -60,6 +60,8 @@ export async function restoreBackup(event: Event) {
|
||||
restoreFromFile(csvLayoutToJson(text))
|
||||
} else if (isCsvChords(text)) {
|
||||
restoreFromFile(csvChordsToJson(text))
|
||||
} else {
|
||||
alert("Unknown backup format")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +134,8 @@ export function getChangesFromChordFile(file: CharaChordFile) {
|
||||
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
||||
const changes: Change[] = []
|
||||
for (const [id, value] of file.settings.entries()) {
|
||||
if (get(settings)[id].value !== value) {
|
||||
const setting = get(settings)[id]
|
||||
if (setting !== undefined && setting.value !== value) {
|
||||
changes.push({
|
||||
type: ChangeType.Setting,
|
||||
id,
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import {KEYMAP_IDS} from "$lib/serial/keymap-codes"
|
||||
import type {CharaChordFile} from "$lib/share/chara-file"
|
||||
|
||||
const SPECIAL_KEYS = new Map<string, string>([[" ", "SPACE"]])
|
||||
|
||||
export function csvChordsToJson(csv: string): CharaChordFile {
|
||||
return {
|
||||
charaVersion: 1,
|
||||
type: "chords",
|
||||
chords: csv.split("\n").map(line => {
|
||||
const [input, output] = line.split(",", 2)
|
||||
return [
|
||||
input.split("+").map(it => KEYMAP_IDS.get(it.trim())?.code ?? 0),
|
||||
output.split("").map(it => KEYMAP_IDS.get(it.trim())?.code ?? 0),
|
||||
]
|
||||
}),
|
||||
chords: csv
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map(line => {
|
||||
const [input, output] = line.split(/,(?=[^,]*$)/, 2)
|
||||
return [
|
||||
input.split("+").map(it => KEYMAP_IDS.get(it.trim())?.code ?? 0),
|
||||
output
|
||||
.trim()
|
||||
.split("")
|
||||
.map(it => KEYMAP_IDS.get(SPECIAL_KEYS.get(it) ?? it)?.code ?? 0),
|
||||
]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function isCsvChords(csv: string): boolean {
|
||||
return /^([^+,\s]( *\+ *[^+,\s]+)* *, *[^+,\s]+ *(\n|(?=$)))+$/.test(csv)
|
||||
return /^([^+]+( *\+ *[^+]+)* *, *[^+, ]+ *(\n|(?=$)))+$/.test(csv)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function csvLayoutToJson(csv: string, device: CharaLayoutFile["device"] =
|
||||
layout: [[], [], []],
|
||||
}
|
||||
|
||||
for (const layer of csv.split("\n")) {
|
||||
for (const layer of csv.trim().split("\n")) {
|
||||
const [layerId, key, action] = layer.substring(1).split(",").map(Number)
|
||||
|
||||
layout.layout[Number(layerId) - 1][Number(key)] = Number(action)
|
||||
|
||||
@@ -2,23 +2,49 @@
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||
import {action as title} from "$lib/title"
|
||||
import {osLayout} from "$lib/os-layout"
|
||||
import LL from "../../i18n/i18n-svelte"
|
||||
|
||||
export let action: number | KeyInfo
|
||||
export let display: "inline-keys" | "keys" = "inline-keys"
|
||||
|
||||
$: info = typeof action === "number" ? KEYMAP_CODES[action] ?? {code: action} : action
|
||||
$: dynamicMapping = info.keyCode && $osLayout.get(info.keyCode)
|
||||
|
||||
$: tooltip =
|
||||
(info.title ?? info.id ?? `0x${info.code.toString(16)}`) +
|
||||
(info.variant === "left" ? " (left)" : info.variant === "right" ? " (right)" : "")
|
||||
</script>
|
||||
|
||||
{#if display === "keys"}
|
||||
<kbd class:icon={!!info.icon} use:title={{title: info.title ?? info.id}}>
|
||||
{info.icon ?? info.id ?? `0x${info.code.toString(16)}`}
|
||||
{#if dynamicMapping}
|
||||
<span
|
||||
use:title={{title: $LL.actionSearch.LIVE_LAYOUT_INFO()}}
|
||||
class="dynamic"
|
||||
class:left={info.variant === "left"}
|
||||
class:right={info.variant === "right"}
|
||||
class:inline={display === "inline-keys"}>{dynamicMapping}</span
|
||||
>
|
||||
{:else if display === "keys"}
|
||||
<kbd
|
||||
class:icon={!!info.icon}
|
||||
class:left={info.variant === "left"}
|
||||
class:right={info.variant === "right"}
|
||||
use:title={{title: tooltip}}
|
||||
>
|
||||
{info.icon ?? info.display ?? info.id ?? `0x${info.code.toString(16)}`}
|
||||
</kbd>
|
||||
{:else if display === "inline-keys"}
|
||||
{#if !info.icon && info.id?.length === 1}
|
||||
<span>{info.id}</span>
|
||||
<span class:left={info.variant === "left"} class:right={info.variant === "right"}>{info.id}</span>
|
||||
{:else}
|
||||
<kbd class="inline-kbd" class:icon={!!info.icon} use:title={{title: info.title ?? info.id}}>
|
||||
{info.icon ?? info.id ?? `0x${info.code.toString(16)}`}</kbd
|
||||
<kbd
|
||||
class="inline-kbd"
|
||||
class:left={info.variant === "left"}
|
||||
class:right={info.variant === "right"}
|
||||
class:icon={!!info.icon}
|
||||
use:title={{title: tooltip}}
|
||||
>
|
||||
{info.icon ?? info.display ?? info.id ?? `0x${info.code.toString(16)}`}</kbd
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -30,6 +56,24 @@
|
||||
transition: color 250ms ease;
|
||||
}
|
||||
|
||||
.left {
|
||||
border-left-width: 3px;
|
||||
}
|
||||
.right {
|
||||
border-right-width: 3px;
|
||||
}
|
||||
|
||||
.dynamic {
|
||||
padding: 4px;
|
||||
border-radius: 1px;
|
||||
min-width: 8px;
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
|
||||
&.inline {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-kbd {
|
||||
margin-inline-end: 2px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||
import LL from "../../i18n/i18n-svelte"
|
||||
import Action from "$lib/components/Action.svelte"
|
||||
|
||||
export let id: number | KeyInfo
|
||||
|
||||
@@ -21,8 +23,14 @@
|
||||
{#if key.description}
|
||||
<i>{key.description}</i>
|
||||
{/if}
|
||||
{#if key.category.name === "ASCII Macros"}
|
||||
<span class="warning">{@html $LL.actionSearch.SHIFT_WARNING()}</span>
|
||||
{/if}
|
||||
{#if key.category.name === "CP-1252"}
|
||||
<span class="warning">{@html $LL.actionSearch.ALT_CODE_WARNING()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<kbd class:icon={!!key.icon}>{key.icon || key.id || `0x${key.code.toString(16)}`}</kbd>
|
||||
<Action display="keys" action={key} />
|
||||
{:else}
|
||||
<span class="key">0x{key.toString(16)}</span>
|
||||
{/if}
|
||||
@@ -63,7 +71,14 @@
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
kbd {
|
||||
height: 24px;
|
||||
.warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--md-sys-color-error);
|
||||
|
||||
> :global(.icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
</script>
|
||||
|
||||
{#if $needRefresh}
|
||||
<button title="Update ready" class="icon" on:click={() => updateServiceWorker(true)}>update</button>
|
||||
<button title="Update ready" on:click={() => updateServiceWorker(true)}
|
||||
>Update <span class="icon">update</span></button
|
||||
>
|
||||
{:else if $offlineReady}
|
||||
<div title="App can now be used offline" class="icon">offline_pin</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {action} from "$lib/title"
|
||||
|
||||
export let currentAction: number | undefined = undefined
|
||||
export let nextAction: number | undefined = undefined
|
||||
|
||||
const index = new Index({tokenize: "full"})
|
||||
for (const action of Object.values(KEYMAP_CODES)) {
|
||||
@@ -121,6 +122,12 @@
|
||||
<h3>{$LL.actionSearch.CURRENT_ACTION()}</h3>
|
||||
<ActionListItem id={currentAction} />
|
||||
</aside>
|
||||
{#if nextAction}
|
||||
<aside>
|
||||
<h3>{$LL.actionSearch.NEXT_ACTION()}</h3>
|
||||
<ActionListItem id={nextAction} />
|
||||
</aside>
|
||||
{/if}
|
||||
{/if}
|
||||
<ul bind:this={resultList}>
|
||||
{#if exact !== undefined}
|
||||
@@ -238,7 +245,7 @@
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--md-sys-color-primary-container);
|
||||
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte"
|
||||
import {getContext} from "svelte"
|
||||
import type {VisualLayoutConfig} from "./visual-layout.js"
|
||||
import {changes, ChangeType} from "$lib/undo-redo"
|
||||
import {changes, ChangeType, layout} from "$lib/undo-redo"
|
||||
|
||||
const {scale, margin, strokeWidth, fontSize, iconFontSize} =
|
||||
getContext<VisualLayoutConfig>("visual-layout-config")
|
||||
@@ -113,9 +113,14 @@
|
||||
function edit(index: number) {
|
||||
const keyInfo = layoutInfo.keys[index]
|
||||
const clickedGroup = groupParent.children.item(index) as SVGGElement
|
||||
const nextAction = get(layout)[get(activeLayer)][keyInfo.id]
|
||||
const currentAction = get(deviceLayout)[get(activeLayer)][keyInfo.id]
|
||||
const component = new ActionSelector({
|
||||
target: document.body,
|
||||
props: {currentAction: get(deviceLayout)[get(activeLayer)][keyInfo.id]},
|
||||
props: {
|
||||
currentAction,
|
||||
nextAction: nextAction.isApplied ? undefined : nextAction.action,
|
||||
},
|
||||
})
|
||||
const dialog = document.querySelector("dialog > div") as HTMLDivElement
|
||||
const backdrop = document.querySelector("dialog") as HTMLDialogElement
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
|
||||
import type {CompiledLayoutKey} from "$lib/serialization/visual-layout"
|
||||
import {layout} from "$lib/undo-redo.js"
|
||||
import {osLayout} from "$lib/os-layout.js"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import {action} from "$lib/title"
|
||||
|
||||
@@ -23,9 +24,17 @@
|
||||
|
||||
{#each positions as position, layer}
|
||||
{@const {action: actionId, isApplied} = $layout[layer][key.id] ?? {action: 0, isApplied: true}}
|
||||
{@const {code, icon, id, title} = KEYMAP_CODES[actionId] ?? {code: actionId}}
|
||||
{@const {code, icon, id, display, title, keyCode, variant} = KEYMAP_CODES[actionId] ?? {code: actionId}}
|
||||
{@const dynamicMapping = keyCode && $osLayout.get(keyCode)}
|
||||
{@const tooltip =
|
||||
(title ?? id ?? `0x${code.toString(16)}`) +
|
||||
(variant === "left" ? " (left)" : variant === "right" ? " (right)" : "")}
|
||||
{@const isActive = layer === $activeLayer}
|
||||
{@const direction = [(middle[0] - margin * 3) / position[0], (middle[1] - margin * 3) / position[1]]}
|
||||
{@const direction = [
|
||||
Math.sign(middle[0]) * (Math.abs(middle[0]) - margin * 3) * position[0],
|
||||
Math.sign(middle[1]) * (Math.abs(middle[1]) - margin * 3) * position[1],
|
||||
]}
|
||||
{@const hasIcon = !dynamicMapping && !!icon}
|
||||
<text
|
||||
fill={isApplied ? "currentcolor" : "var(--md-sys-color-primary)"}
|
||||
font-weight={isApplied ? "" : "bold"}
|
||||
@@ -33,16 +42,18 @@
|
||||
alignment-baseline="central"
|
||||
x={pos[0] + middle[0] + (isApplied ? 0 : fontSize / 3)}
|
||||
y={pos[1] + middle[1]}
|
||||
font-size={fontSizeMultiplier * (icon ? iconFontSize : fontSize)}
|
||||
font-family={icon ? "Material Symbols Rounded" : undefined}
|
||||
font-size={fontSizeMultiplier * (hasIcon ? iconFontSize : fontSize)}
|
||||
font-family={hasIcon ? "Material Symbols Rounded" : undefined}
|
||||
opacity={isActive ? 1 : inactiveOpacity}
|
||||
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].toPrecision(2)}px ${direction[1].toPrecision(2)}px 0`}
|
||||
style:rotate="{rotate}deg"
|
||||
use:action={{title: title ?? id}}
|
||||
use:action={{title: tooltip}}
|
||||
>
|
||||
{#if code !== 0}
|
||||
{icon || id || `0x${code.toString(16)}`}
|
||||
{dynamicMapping || icon || display || id || `0x${code.toString(16)}`}
|
||||
{/if}
|
||||
{#if !isApplied}
|
||||
<tspan>•</tspan>
|
||||
@@ -56,6 +67,7 @@
|
||||
|
||||
text {
|
||||
will-change: translate, scale;
|
||||
user-select: none;
|
||||
transform-origin: center;
|
||||
transform-box: fill-box;
|
||||
transition:
|
||||
@@ -64,4 +76,8 @@
|
||||
translate #{$transition} ease,
|
||||
scale #{$transition} ease;
|
||||
}
|
||||
|
||||
text:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,23 +42,30 @@
|
||||
{@const r2 = r1 - sizeY + innerMargin * 2}
|
||||
{@const p2 = r2 - innerMargin}
|
||||
{@const multiplier = 1.25}
|
||||
<g style:transform="rotateZ({key.rotate}deg) translate({innerMargin}px, {innerMargin}px)">
|
||||
<path
|
||||
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
|
||||
{key}
|
||||
middle={[sizeY - margin * 2, sizeY - margin * 2]}
|
||||
pos={[posX, posY]}
|
||||
rotate={-key.rotate}
|
||||
fontSizeMultiplier={multiplier}
|
||||
positions={[
|
||||
[-0.5, -0.5],
|
||||
[0.5, -0.5],
|
||||
[-0.5, 0.5],
|
||||
]}
|
||||
/>
|
||||
</g>
|
||||
|
||||
{@const rotateRad = (key.rotate + 45) * (Math.PI / 180)}
|
||||
{@const rotX = Math.round((Math.abs(Math.cos(rotateRad - Math.PI / 2)) + Number.EPSILON) * 100) / 100}
|
||||
{@const rotY = Math.round((Math.abs(Math.sin(rotateRad - Math.PI / 2)) + Number.EPSILON) * 100) / 100}
|
||||
|
||||
{@const rc = r1 - (r1 - r2) / 2}
|
||||
{@const middleX = Math.cos(rotateRad) * rc}
|
||||
{@const middleY = Math.sin(rotateRad) * rc}
|
||||
<path
|
||||
style:transform="rotateZ({key.rotate}deg) translate({innerMargin}px, {innerMargin}px)"
|
||||
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
|
||||
{key}
|
||||
middle={[middleX, middleY]}
|
||||
pos={[posX, posY]}
|
||||
rotate={0}
|
||||
fontSizeMultiplier={multiplier}
|
||||
positions={[
|
||||
[-rotY, -rotX],
|
||||
[-rotX, -rotY],
|
||||
[rotX, rotY],
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
</g>
|
||||
|
||||
@@ -71,6 +78,7 @@
|
||||
transform-box: fill-box;
|
||||
}
|
||||
|
||||
path,
|
||||
g {
|
||||
transform-origin: top left;
|
||||
transform-box: fill-box;
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
import {persistentWritable} from "$lib/storage"
|
||||
import {get} from "svelte/store"
|
||||
import {get, writable} from "svelte/store"
|
||||
|
||||
export const osLayout = persistentWritable<Record<string, string>>("os-layout", {})
|
||||
export const osLayout = writable<Map<string, string>>(new Map())
|
||||
|
||||
const keysCurrentlyDown = new Set<string>()
|
||||
|
||||
function keydown({code, key}: KeyboardEvent) {
|
||||
const keys = [...keysCurrentlyDown]
|
||||
keysCurrentlyDown.add(code)
|
||||
|
||||
const keyString = JSON.stringify([...keys.sort(), code])
|
||||
if (keyString in get(osLayout) || get(osLayout)[JSON.stringify([code])] === key) return
|
||||
|
||||
osLayout.update(layout => {
|
||||
layout[keyString] = key
|
||||
return layout
|
||||
})
|
||||
}
|
||||
|
||||
function keyup({code}: KeyboardEvent) {
|
||||
keysCurrentlyDown.delete(code)
|
||||
}
|
||||
|
||||
export function runLayoutDetection() {
|
||||
if ("keyboard" in navigator) {
|
||||
;(navigator.keyboard as any).getLayoutMap().then((layout: Map<string, string>) => {
|
||||
osLayout.update(osLayout => {
|
||||
Object.assign(
|
||||
osLayout,
|
||||
Object.fromEntries([...layout.entries()].map(([key, value]) => [JSON.stringify([key]), value])),
|
||||
)
|
||||
return osLayout
|
||||
})
|
||||
})
|
||||
async function updateLayout() {
|
||||
const layout: Map<string, string> = await (navigator as any).keyboard.getLayoutMap()
|
||||
const currentLayout = get(osLayout)
|
||||
if (
|
||||
layout.size !== currentLayout.size ||
|
||||
[...layout.keys()].some(key => layout.get(key) !== currentLayout.get(key))
|
||||
) {
|
||||
osLayout.set(layout)
|
||||
}
|
||||
}
|
||||
|
||||
export function runLayoutDetection(): () => void {
|
||||
if ("keyboard" in navigator) {
|
||||
updateLayout()
|
||||
const timer = setInterval(updateLayout, 5000)
|
||||
return () => clearInterval(timer)
|
||||
} else {
|
||||
console.warn("Keyboard API not supported")
|
||||
return () => {}
|
||||
}
|
||||
window.addEventListener("keydown", keydown)
|
||||
window.addEventListener("keyup", keyup)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||
["X", {usbProductId: 33163, usbVendorId: 12346}],
|
||||
])
|
||||
|
||||
const KEY_COUNTS = {
|
||||
ONE: 90,
|
||||
LITE: 67,
|
||||
X: 256,
|
||||
} as const
|
||||
|
||||
if (browser && navigator.serial === undefined && import.meta.env.TAURI_FAMILY !== undefined) {
|
||||
await import("./tauri-serial")
|
||||
}
|
||||
@@ -45,11 +51,18 @@ export class CharaDevice {
|
||||
|
||||
private lock?: Promise<true>
|
||||
|
||||
private readonly suspendDebounce = 100
|
||||
private suspendDebounceId?: number
|
||||
|
||||
version!: SemVer
|
||||
company!: "CHARACHORDER"
|
||||
device!: "ONE" | "LITE"
|
||||
device!: "ONE" | "LITE" | "X"
|
||||
chipset!: "M0" | "S2"
|
||||
keyCount!: 90 | 67
|
||||
keyCount!: 90 | 67 | 256
|
||||
|
||||
get portInfo() {
|
||||
return this.port.getInfo()
|
||||
}
|
||||
|
||||
constructor(private readonly baudRate = 115200) {}
|
||||
|
||||
@@ -77,9 +90,9 @@ export class CharaDevice {
|
||||
this.version = new SemVer(await this.send("VERSION").then(([version]) => version))
|
||||
const [company, device, chipset] = await this.send("ID")
|
||||
this.company = company as "CHARACHORDER"
|
||||
this.device = device as "ONE" | "LITE"
|
||||
this.device = device as "ONE" | "LITE" | "X"
|
||||
this.chipset = chipset as "M0" | "S2"
|
||||
this.keyCount = this.device === "ONE" ? 90 : 67
|
||||
this.keyCount = KEY_COUNTS[this.device]
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
console.error(e)
|
||||
@@ -159,11 +172,23 @@ export class CharaDevice {
|
||||
const exec = new Promise<T>(async resolve => {
|
||||
let result!: T
|
||||
try {
|
||||
await this.wake()
|
||||
if (this.suspendDebounceId) {
|
||||
clearTimeout(this.suspendDebounceId)
|
||||
} else {
|
||||
await this.wake()
|
||||
}
|
||||
result = await callback(send, read)
|
||||
} finally {
|
||||
await this.suspend()
|
||||
this.lock = undefined
|
||||
delete this.lock
|
||||
this.suspendDebounceId = setTimeout(() => {
|
||||
// cannot be locked here as all the code until clearTimeout is sync
|
||||
console.assert(this.lock === undefined)
|
||||
this.lock = this.suspend().then(() => {
|
||||
delete this.lock
|
||||
delete this.suspendDebounceId
|
||||
return true
|
||||
})
|
||||
}, this.suspendDebounce) as any
|
||||
resolve(result)
|
||||
}
|
||||
})
|
||||
@@ -265,7 +290,7 @@ export class CharaDevice {
|
||||
* To permanently store the settings, you *must* call commit.
|
||||
*/
|
||||
async setSetting(id: number, value: number) {
|
||||
const [status] = await this.send(`VAR B2 ${id} ${value}`)
|
||||
const [status] = await this.send(`VAR B2 ${id.toString(16).toUpperCase()} ${value}`)
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||
}
|
||||
|
||||
@@ -273,8 +298,9 @@ export class CharaDevice {
|
||||
* Retrieves a setting from the device
|
||||
*/
|
||||
async getSetting(id: number): Promise<number> {
|
||||
const [value, status] = await this.send(`VAR B1 ${id}`)
|
||||
if (status !== "0") throw new Error(`Setting "${id}" doesn't exist (Status code ${status})`)
|
||||
const [value, status] = await this.send(`VAR B1 ${id.toString(16).toUpperCase()}`)
|
||||
if (status !== "0")
|
||||
throw new Error(`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`)
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
@@ -283,7 +309,6 @@ export class CharaDevice {
|
||||
*/
|
||||
async reboot() {
|
||||
await this.send("RST")
|
||||
// TODO: reconnect
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,7 +316,13 @@ export class CharaDevice {
|
||||
*/
|
||||
async bootloader() {
|
||||
await this.send("RST BOOTLOADER")
|
||||
// TODO: more...
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the device
|
||||
*/
|
||||
async reset(type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC") {
|
||||
await this.send(`RST ${type}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,12 @@ export const KEYMAP_CODES: Record<number, KeyInfo> = Object.fromEntries(
|
||||
),
|
||||
)
|
||||
|
||||
export const KEYMAP_KEYCODES: Map<string, number> = new Map(
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
Object.entries(category.actions).map(([code, action]) => [action.keyCode!, Number(code)] as const),
|
||||
).filter(([keyCode]) => keyCode !== undefined),
|
||||
)
|
||||
|
||||
export const KEYMAP_IDS: Map<string, KeyInfo> = new Map(
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
Object.entries(category.actions).map(
|
||||
|
||||
12
src/lib/serial/updater.ts
Normal file
12
src/lib/serial/updater.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export async function updateDevice(port: SerialPort) {
|
||||
await port.open({
|
||||
baudRate: 115200,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: "none",
|
||||
bufferSize: 255,;
|
||||
})
|
||||
|
||||
const writer = port.writable!.getWriter()
|
||||
const reader = port.readable!.getReader()
|
||||
}
|
||||
@@ -7,6 +7,9 @@ export const setting: Action<HTMLInputElement, {id: number; inverse?: number; sc
|
||||
) {
|
||||
node.setAttribute("disabled", "")
|
||||
const type = node.getAttribute("type") as "number" | "checkbox"
|
||||
const min = node.hasAttribute("min") ? Number(node.getAttribute("min")) : undefined
|
||||
const max = node.hasAttribute("max") ? Number(node.getAttribute("max")) : undefined
|
||||
console.log(min, max, "|", id, "|", node.getAttribute("min"), node.getAttribute("max"))
|
||||
|
||||
const unsubscribe = settings.subscribe(async settings => {
|
||||
if (id in settings) {
|
||||
@@ -32,9 +35,13 @@ export const setting: Action<HTMLInputElement, {id: number; inverse?: number; sc
|
||||
async function listener() {
|
||||
let value: number
|
||||
if (type === "number") {
|
||||
value = Number.parseInt(node.value)
|
||||
value = Number(node.value)
|
||||
if (Number.isNaN(value)) return
|
||||
value = inverse !== undefined ? inverse / value : scale !== undefined ? value / scale : value
|
||||
value = Math.floor(
|
||||
inverse !== undefined ? inverse / value : scale !== undefined ? value / scale : value,
|
||||
)
|
||||
if (min !== undefined) value = Math.max(min, value)
|
||||
if (max !== undefined) value = Math.min(max, value)
|
||||
} else {
|
||||
value = node.checked ? 1 : 0
|
||||
}
|
||||
@@ -48,11 +55,12 @@ export const setting: Action<HTMLInputElement, {id: number; inverse?: number; sc
|
||||
return changes
|
||||
})
|
||||
}
|
||||
node.addEventListener("input", listener)
|
||||
|
||||
node.addEventListener("change", listener)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("input", listener)
|
||||
node.removeEventListener("change", listener)
|
||||
unsubscribe()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ export const action: Action<Element, {title?: string; shortcut?: string}> = (
|
||||
}
|
||||
|
||||
return {
|
||||
update(updated) {
|
||||
title = updated.title
|
||||
shortcut = updated.shortcut
|
||||
},
|
||||
destroy() {
|
||||
tooltip.destroy()
|
||||
hotkeys.unbind(shortcut)
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface LayoutChange {
|
||||
|
||||
export interface ChordChange {
|
||||
type: ChangeType.Chord
|
||||
deleted?: true
|
||||
id: number[]
|
||||
actions: number[]
|
||||
phrase: number[]
|
||||
@@ -41,7 +42,7 @@ export const changes = persistentWritable<Change[]>("changes", [])
|
||||
|
||||
export interface Overlay {
|
||||
layout: [Map<number, number>, Map<number, number>, Map<number, number>]
|
||||
chords: Map<string, Chord>
|
||||
chords: Map<string, Chord & {deleted: boolean}>
|
||||
settings: Map<number, number>
|
||||
}
|
||||
|
||||
@@ -58,7 +59,11 @@ export const overlay = derived(changes, changes => {
|
||||
overlay.layout[change.layer].set(change.id, change.action)
|
||||
break
|
||||
case ChangeType.Chord:
|
||||
overlay.chords.set(JSON.stringify(change.id), {actions: change.actions, phrase: change.phrase})
|
||||
overlay.chords.set(JSON.stringify(change.id), {
|
||||
actions: change.actions,
|
||||
phrase: change.phrase,
|
||||
deleted: change.deleted ?? false,
|
||||
})
|
||||
break
|
||||
case ChangeType.Setting:
|
||||
overlay.settings.set(change.id, change.setting)
|
||||
@@ -88,7 +93,10 @@ export const layout = derived([overlay, deviceLayout], ([overlay, layout]) =>
|
||||
)
|
||||
|
||||
export type ChordInfo = Chord &
|
||||
ChangeInfo & {phraseChanged: boolean; actionsChanged: boolean; sortBy: string} & {id: number[]}
|
||||
ChangeInfo & {phraseChanged: boolean; actionsChanged: boolean; sortBy: string} & {
|
||||
id: number[]
|
||||
deleted: boolean
|
||||
}
|
||||
export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
|
||||
const newChords = new Set(overlay.chords.keys())
|
||||
|
||||
@@ -106,6 +114,7 @@ export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
|
||||
actionsChanged: id !== JSON.stringify(changedChord.actions),
|
||||
phraseChanged: JSON.stringify(chord.phrase) !== JSON.stringify(changedChord.phrase),
|
||||
isApplied: false,
|
||||
deleted: changedChord.deleted,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
@@ -116,6 +125,7 @@ export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
|
||||
phraseChanged: false,
|
||||
actionsChanged: false,
|
||||
isApplied: true,
|
||||
deleted: false,
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -126,6 +136,7 @@ export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
|
||||
isApplied: false,
|
||||
actionsChanged: true,
|
||||
phraseChanged: false,
|
||||
deleted: chord.deleted,
|
||||
id: JSON.parse(id),
|
||||
phrase: chord.phrase,
|
||||
actions: chord.actions,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import "$lib/style/scrollbar.scss"
|
||||
import "$lib/style/tippy.scss"
|
||||
import "$lib/style/theme.scss"
|
||||
import {onMount} from "svelte"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import {applyTheme, argbFromHex, themeFromSourceColor} from "@material/material-color-utilities"
|
||||
import Navigation from "./Navigation.svelte"
|
||||
import {canAutoConnect} from "$lib/serial/device"
|
||||
@@ -23,16 +23,16 @@
|
||||
import Footer from "./Footer.svelte"
|
||||
import {runLayoutDetection} from "$lib/os-layout.js"
|
||||
import PageTransition from "./PageTransition.svelte"
|
||||
import SyncOverlay from "./SyncOverlay.svelte"
|
||||
import {restoreFromFile} from "$lib/backup/backup"
|
||||
import {goto} from "$app/navigation"
|
||||
|
||||
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
|
||||
loadLocale(locale)
|
||||
setLocale(locale)
|
||||
let stopLayoutDetection: () => void
|
||||
|
||||
if (browser) {
|
||||
runLayoutDetection()
|
||||
stopLayoutDetection = runLayoutDetection()
|
||||
tippy.setDefaultProps({
|
||||
animation: "shift-away",
|
||||
theme: "surface-variant",
|
||||
@@ -53,7 +53,7 @@
|
||||
})
|
||||
if (import.meta.env.TAURI_FAMILY === undefined) {
|
||||
const {initPwa} = await import("./pwa-setup")
|
||||
await initPwa()
|
||||
webManifestLink = await initPwa()
|
||||
}
|
||||
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
||||
@@ -67,6 +67,10 @@
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
stopLayoutDetection?.()
|
||||
})
|
||||
|
||||
let webManifestLink = ""
|
||||
</script>
|
||||
|
||||
@@ -77,8 +81,6 @@
|
||||
<meta name="theme-color" content={data.themeColor} />
|
||||
</svelte:head>
|
||||
|
||||
<SyncOverlay />
|
||||
|
||||
<Navigation />
|
||||
|
||||
<!-- <PickChangesDialog /> -->
|
||||
@@ -115,8 +117,7 @@
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
|
||||
padding: 16px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -2,5 +2,5 @@ import {redirect} from "@sveltejs/kit"
|
||||
import type {PageLoad} from "./$types"
|
||||
|
||||
export const load = (() => {
|
||||
throw redirect(302, "/config")
|
||||
throw redirect(302, "/config/")
|
||||
}) satisfies PageLoad
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
>{$LL.browserWarning.INFO_BROWSER_SUFFIX()}
|
||||
</p>
|
||||
<div>
|
||||
<a href="https://github.com/CharaChorder/DeviceManager/releases" target="_blank"
|
||||
>{$LL.browserWarning.DOWNLOAD_APP()}</a
|
||||
>
|
||||
<p>{$LL.browserWarning.DOWNLOAD_APP()}</p>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -50,9 +48,10 @@
|
||||
|
||||
a {
|
||||
color: var(--md-sys-color-on-error);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div > a {
|
||||
div > p {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
@@ -5,8 +5,36 @@
|
||||
import {preference} from "$lib/preferences"
|
||||
import LL from "../i18n/i18n-svelte"
|
||||
|
||||
function reboot() {
|
||||
$serialPort?.reboot()
|
||||
$serialPort = undefined
|
||||
powerDialog = false
|
||||
setTimeout(() => {
|
||||
initSerial()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function bootloader() {
|
||||
$serialPort?.bootloader()
|
||||
$serialPort = undefined
|
||||
rebootInfo = true
|
||||
powerDialog = false
|
||||
}
|
||||
|
||||
async function updateFirmware() {
|
||||
const {usbVendorId: vendorId, usbProductId: productId} = $serialPort!.portInfo
|
||||
$serialPort!.bootloader()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
console.log(await navigator.usb.requestDevice({filters: [{vendorId, productId}]}))
|
||||
}
|
||||
|
||||
let rebootInfo = false
|
||||
let terminal = false
|
||||
let powerDialog = false
|
||||
|
||||
$: if ($serialPort) {
|
||||
rebootInfo = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@@ -23,9 +51,43 @@
|
||||
<br />
|
||||
Version {$serialPort.version}
|
||||
</p>
|
||||
<!--<button on:click={updateFirmware}>Update</button>-->
|
||||
{/if}
|
||||
|
||||
{#if browser}
|
||||
{#if navigator.userAgent.includes("Linux") && !$serialPort}
|
||||
<details class="linux-info" transition:slide>
|
||||
<summary>{@html $LL.deviceManager.LINUX_PERMISSIONS()}</summary>
|
||||
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>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>
|
||||
</details>
|
||||
{/if}
|
||||
{#if rebootInfo}
|
||||
<p transition:slide><b>{$LL.deviceManager.bootMenu.POWER_WARNING()}</b></p>
|
||||
{/if}
|
||||
<div class="row">
|
||||
{#if $serialPort}
|
||||
<button
|
||||
@@ -69,17 +131,11 @@
|
||||
/>
|
||||
<dialog open transition:slide={{duration: 250}}>
|
||||
<h3>{$LL.deviceManager.bootMenu.TITLE()}</h3>
|
||||
<button
|
||||
on:click={() => {
|
||||
$serialPort?.reboot()
|
||||
$serialPort = undefined
|
||||
}}><span class="icon">restart_alt</span>{$LL.deviceManager.bootMenu.REBOOT()}</button
|
||||
<button on:click={reboot}
|
||||
><span class="icon">restart_alt</span>{$LL.deviceManager.bootMenu.REBOOT()}</button
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
$serialPort?.bootloader()
|
||||
$serialPort = undefined
|
||||
}}><span class="icon">rule_settings</span>{$LL.deviceManager.bootMenu.BOOTLOADER()}</button
|
||||
<button on:click={bootloader}
|
||||
><span class="icon">rule_settings</span>{$LL.deviceManager.bootMenu.BOOTLOADER()}</button
|
||||
>
|
||||
</dialog>
|
||||
{/if}
|
||||
@@ -95,13 +151,28 @@
|
||||
margin-block: 8px;
|
||||
}
|
||||
|
||||
details a {
|
||||
display: inline;
|
||||
padding-inline: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
min-width: 260px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import LL from "../i18n/i18n-svelte"
|
||||
import {changes, ChangeType, chords, layout, overlay, settings} from "$lib/undo-redo"
|
||||
import type {Change} from "$lib/undo-redo"
|
||||
import {fly, slide} from "svelte/transition"
|
||||
import {fly} from "svelte/transition"
|
||||
import {action} from "$lib/title"
|
||||
import {
|
||||
deviceChords,
|
||||
@@ -35,87 +35,90 @@
|
||||
let redoQueue: Change[] = []
|
||||
|
||||
async function save() {
|
||||
const port = $serialPort
|
||||
if (!port) return
|
||||
$syncStatus = "uploading"
|
||||
try {
|
||||
const port = $serialPort
|
||||
if (!port) return
|
||||
$syncStatus = "uploading"
|
||||
|
||||
for (const [id, {actions, phrase}] of $overlay.chords) {
|
||||
if (phrase.length > 0) {
|
||||
if (id !== JSON.stringify(actions)) {
|
||||
const existingChord = await port.getChordPhrase(actions)
|
||||
if (
|
||||
existingChord !== undefined &&
|
||||
!(await askForConfirmation(
|
||||
$LL.configure.chords.conflict.TITLE(),
|
||||
$LL.configure.chords.conflict.DESCRIPTION(
|
||||
actions.map(it => `<kbd>${KEYMAP_CODES[it].id}</kbd>`).join(" "),
|
||||
),
|
||||
$LL.configure.chords.conflict.CONFIRM(),
|
||||
$LL.configure.chords.conflict.ABORT(),
|
||||
))
|
||||
) {
|
||||
changes.update(changes =>
|
||||
changes.filter(it => !(it.type === ChangeType.Chord && JSON.stringify(it.id) === id)),
|
||||
)
|
||||
continue
|
||||
for (const [id, {actions, phrase, deleted}] of $overlay.chords) {
|
||||
if (!deleted) {
|
||||
if (id !== JSON.stringify(actions)) {
|
||||
const existingChord = await port.getChordPhrase(actions)
|
||||
if (
|
||||
existingChord !== undefined &&
|
||||
!(await askForConfirmation(
|
||||
$LL.configure.chords.conflict.TITLE(),
|
||||
$LL.configure.chords.conflict.DESCRIPTION(
|
||||
actions.map(it => `<kbd>${KEYMAP_CODES[it].id}</kbd>`).join(" "),
|
||||
),
|
||||
$LL.configure.chords.conflict.CONFIRM(),
|
||||
$LL.configure.chords.conflict.ABORT(),
|
||||
))
|
||||
) {
|
||||
changes.update(changes =>
|
||||
changes.filter(it => !(it.type === ChangeType.Chord && JSON.stringify(it.id) === id)),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
await port.deleteChord({actions: JSON.parse(id)})
|
||||
}
|
||||
|
||||
await port.deleteChord({actions: JSON.parse(id)})
|
||||
}
|
||||
await port.setChord({actions, phrase})
|
||||
} else {
|
||||
await port.deleteChord({actions})
|
||||
}
|
||||
}
|
||||
|
||||
for (const [layer, actions] of $overlay.layout.entries()) {
|
||||
for (const [id, action] of actions) {
|
||||
await port.setLayoutKey(layer + 1, id, action)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, setting] of $overlay.settings) {
|
||||
await port.setSetting(id, setting)
|
||||
}
|
||||
|
||||
|
||||
// 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!"
|
||||
const virtualWriteTime = 1000
|
||||
const startStamp = performance.now()
|
||||
await new Promise<void>(resolve => {
|
||||
function animate() {
|
||||
const delta = performance.now() - startStamp
|
||||
syncProgress.set({
|
||||
max: virtualWriteTime,
|
||||
current: delta,
|
||||
})
|
||||
if (delta >= virtualWriteTime) {
|
||||
resolve()
|
||||
await port.setChord({actions, phrase})
|
||||
} else {
|
||||
requestAnimationFrame(animate)
|
||||
await port.deleteChord({actions})
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animate)
|
||||
})
|
||||
await port.commit()
|
||||
|
||||
$deviceLayout = $layout.map(layer => layer.map<number>(({action}) => action)) as [
|
||||
number[],
|
||||
number[],
|
||||
number[],
|
||||
]
|
||||
$deviceChords = $chords
|
||||
.map(({actions, phrase}) => ({actions, phrase}))
|
||||
.filter(({phrase}) => phrase.length > 1)
|
||||
$deviceSettings = $settings.map(({value}) => value)
|
||||
$changes = []
|
||||
$syncStatus = "done"
|
||||
for (const [layer, actions] of $overlay.layout.entries()) {
|
||||
for (const [id, action] of actions) {
|
||||
await port.setLayoutKey(layer + 1, id, action)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, setting] of $overlay.settings) {
|
||||
await port.setSetting(id, setting)
|
||||
}
|
||||
|
||||
// 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!"
|
||||
const virtualWriteTime = 1000
|
||||
const startStamp = performance.now()
|
||||
await new Promise<void>(resolve => {
|
||||
function animate() {
|
||||
const delta = performance.now() - startStamp
|
||||
syncProgress.set({
|
||||
max: virtualWriteTime,
|
||||
current: delta,
|
||||
})
|
||||
if (delta >= virtualWriteTime) {
|
||||
resolve()
|
||||
} else {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animate)
|
||||
})
|
||||
await port.commit()
|
||||
|
||||
$deviceLayout = $layout.map(layer => layer.map<number>(({action}) => action)) as [
|
||||
number[],
|
||||
number[],
|
||||
number[],
|
||||
]
|
||||
$deviceChords = $chords.filter(({deleted}) => !deleted).map(({actions, phrase}) => ({actions, phrase}))
|
||||
$deviceSettings = $settings.map(({value}) => value)
|
||||
$changes = []
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
console.error(e)
|
||||
} finally {
|
||||
$syncStatus = "done"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import {detectLocale, locales} from "../i18n/i18n-util"
|
||||
import {loadLocaleAsync} from "../i18n/i18n-util.async"
|
||||
import {tick} from "svelte"
|
||||
import SyncOverlay from "./SyncOverlay.svelte"
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
|
||||
let locale = (browser && (localStorage.getItem("locale") as Locales)) || detectLocale()
|
||||
$: if (browser)
|
||||
@@ -41,10 +43,28 @@
|
||||
</li>
|
||||
<li>
|
||||
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">bug_report</span> File an issue</a
|
||||
><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
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={import.meta.env.VITE_LEARN_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">school</span> Train</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
{#if !$serialPort}
|
||||
<div class="warning">
|
||||
<span class="icon">warning</span>{$LL.deviceManager.NO_DEVICE()}
|
||||
</div>
|
||||
{/if}
|
||||
<SyncOverlay />
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<input use:action={{title: $LL.profile.theme.COLOR_SCHEME()}} type="color" bind:value={$theme.color} />
|
||||
@@ -83,6 +103,14 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--md-sys-color-error);
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -112,29 +140,32 @@
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
padding-inline-end: 16px;
|
||||
padding-block-start: 0;
|
||||
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
list-style: none;
|
||||
|
||||
&:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
ul:last-child {
|
||||
@@ -153,6 +184,8 @@
|
||||
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -50,15 +50,13 @@
|
||||
<PwaStatus />
|
||||
{/await}
|
||||
{/if}
|
||||
{#if $serialPort}
|
||||
<button use:action={{title: $LL.backup.TITLE()}} use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||
{#if $userPreferences.backup}
|
||||
history
|
||||
{:else}
|
||||
history_toggle_off
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button use:action={{title: $LL.backup.TITLE()}} use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||
{#if $userPreferences.backup}
|
||||
history
|
||||
{:else}
|
||||
history_toggle_off
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
bind:this={connectButton}
|
||||
use:action={{title: $LL.deviceManager.TITLE()}}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
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))) {
|
||||
|
||||
@@ -1,49 +1,30 @@
|
||||
<script lang="ts">
|
||||
import {syncProgress, syncStatus} from "$lib/serial/connection"
|
||||
import LL from "../i18n/i18n-svelte"
|
||||
|
||||
$: if (dialog) toggleDialog($syncStatus)
|
||||
|
||||
async function toggleDialog(status: "uploading" | "downloading" | string) {
|
||||
// debounce
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
if ($syncStatus !== status) return
|
||||
|
||||
if (!dialog.open && ($syncStatus === "uploading" || $syncStatus === "downloading")) {
|
||||
message = $syncStatus
|
||||
dialog.showModal()
|
||||
dialog.animate([{opacity: 0}, {opacity: 1}], {duration: 250, easing: "ease"})
|
||||
} else if (dialog.open) {
|
||||
const animation = dialog.animate([{opacity: 1}, {opacity: 0}], {duration: 250, easing: "ease"})
|
||||
animation.addEventListener("finish", () => {
|
||||
dialog.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let message: "downloading" | "uploading"
|
||||
let dialog: HTMLDialogElement
|
||||
import {fly} from "svelte/transition"
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialog}>
|
||||
{#if message === "downloading"}
|
||||
<h2>{$LL.sync.TITLE_READ()}</h2>
|
||||
{:else}
|
||||
<h2>{$LL.sync.TITLE_WRITE()}</h2>
|
||||
{/if}
|
||||
<progress max={$syncProgress?.max ?? 1} value={$syncProgress?.current ?? 1}></progress>
|
||||
</dialog>
|
||||
{#if $syncStatus !== "done"}
|
||||
<div transition:fly={{y: 40}}>
|
||||
<progress max={$syncProgress?.max ?? 1} value={$syncProgress?.current ?? 1}></progress>
|
||||
{#if $syncStatus === "downloading"}
|
||||
<div>{$LL.sync.TITLE_READ()}</div>
|
||||
{:else}
|
||||
<div>{$LL.sync.TITLE_WRITE()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
dialog::backdrop {
|
||||
background: rgba(0 0 0 / 70%);
|
||||
div {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
progress {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
@@ -53,15 +34,4 @@
|
||||
progress::-webkit-progress-value {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
dialog {
|
||||
max-width: 14cm;
|
||||
padding: 2cm;
|
||||
|
||||
color: white;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
6
src/routes/api/firmware/+page.json
Normal file
6
src/routes/api/firmware/+page.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"CHARACHORDER ONE M0": {
|
||||
"latest": "1.1.3",
|
||||
"next": null
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<h1>Layout Bootcamp</h1>
|
||||
@@ -34,7 +34,13 @@
|
||||
const index = new Index({tokenize: "full"})
|
||||
chords.forEach((chord, i) => {
|
||||
if ("phrase" in chord) {
|
||||
index.add(i, chord.phrase.map(it => KEYMAP_CODES[it].id).join(""))
|
||||
index.add(
|
||||
i,
|
||||
chord.phrase
|
||||
.map(it => KEYMAP_CODES[it]?.id)
|
||||
.filter(it => !!it)
|
||||
.join(""),
|
||||
)
|
||||
}
|
||||
})
|
||||
return index
|
||||
@@ -49,6 +55,11 @@
|
||||
}
|
||||
|
||||
function insertChord(actions: number[]) {
|
||||
const id = JSON.stringify(actions)
|
||||
if ($chords.some(it => JSON.stringify(it.actions) === id)) {
|
||||
alert($LL.configure.chords.DUPLICATE())
|
||||
return
|
||||
}
|
||||
changes.update(changes => {
|
||||
changes.push({
|
||||
type: ChangeType.Chord,
|
||||
@@ -76,7 +87,8 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Chord Manager</title>
|
||||
<title>Chord Manager - CharaChorder Device Manager</title>
|
||||
<meta name="description" content="Manage your chords" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="search-container">
|
||||
@@ -105,7 +117,10 @@
|
||||
<section bind:this={results}>
|
||||
<table>
|
||||
{#if page === 0}
|
||||
<tr><th><ChordActionEdit on:submit={({detail}) => insertChord(detail)} /></th><td /><td /></tr>
|
||||
<tr
|
||||
><th class="new-chord"><ChordActionEdit on:submit={({detail}) => insertChord(detail)} /></th><td /><td
|
||||
/></tr
|
||||
>
|
||||
{/if}
|
||||
{#if $lastPage !== -1}
|
||||
{#each $items.slice(page * $pageSize - (page === 0 ? 0 : 1), (page + 1) * $pageSize - 1) as [chord] (JSON.stringify(chord.id))}
|
||||
@@ -114,9 +129,10 @@
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<caption> No Results </caption>
|
||||
<caption>{$LL.configure.chords.search.NO_RESULTS()}</caption>
|
||||
{/if}
|
||||
</table>
|
||||
<textarea placeholder={$LL.configure.chords.TRY_TYPING()}></textarea>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -132,6 +148,24 @@
|
||||
min-width: 8ch;
|
||||
}
|
||||
|
||||
.new-chord :global(.add) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
textarea {
|
||||
transition: border-color 250ms ease;
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: 1px dashed var(--md-sys-color-surface-variant);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
caption {
|
||||
margin-top: 156px;
|
||||
}
|
||||
@@ -164,6 +198,7 @@
|
||||
|
||||
section {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
@@ -174,6 +209,7 @@
|
||||
}
|
||||
|
||||
table {
|
||||
height: fit-content;
|
||||
overflow: hidden;
|
||||
min-width: min(90vw, 16.5cm);
|
||||
transition: all 1s ease;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_IDS} from "$lib/serial/keymap-codes"
|
||||
import type {ChordInfo} from "$lib/undo-redo"
|
||||
import {changes, ChangeType} from "$lib/undo-redo"
|
||||
import {createEventDispatcher} from "svelte"
|
||||
import LL from "../../../i18n/i18n-svelte"
|
||||
import ActionString from "$lib/components/ActionString.svelte"
|
||||
import {selectAction} from "./action-selector"
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
import {get} from "svelte/store"
|
||||
import {inputToAction} from "./input-converter"
|
||||
|
||||
export let chord: ChordInfo | undefined = undefined
|
||||
|
||||
@@ -25,7 +28,7 @@
|
||||
function keydown(event: KeyboardEvent) {
|
||||
if (!editing) return
|
||||
event.preventDefault()
|
||||
pressedKeys.add(KEYMAP_IDS.get(event.key)!.code)
|
||||
pressedKeys.add(inputToAction(event, get(serialPort)?.device === "X")!)
|
||||
pressedKeys = pressedKeys
|
||||
}
|
||||
|
||||
@@ -44,12 +47,27 @@
|
||||
return changes
|
||||
})
|
||||
}
|
||||
|
||||
function addSpecial(event: MouseEvent) {
|
||||
selectAction(event, action => {
|
||||
changes.update(changes => {
|
||||
changes.push({
|
||||
type: ChangeType.Chord,
|
||||
id: chord!.id,
|
||||
actions: [...chord!.actions, action].sort(compare),
|
||||
phrase: chord!.phrase,
|
||||
})
|
||||
return changes
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class:deleted={chord && chord.phrase.length === 0}
|
||||
class:deleted={chord && chord.deleted}
|
||||
class:edited={chord && chord.actionsChanged}
|
||||
class:invalid={chord && chord.actions.toSorted(compare).some((it, i) => chord?.actions[i] !== it)}
|
||||
class="chord"
|
||||
on:click={edit}
|
||||
on:keydown={keydown}
|
||||
on:keyup={keyup}
|
||||
@@ -60,6 +78,7 @@
|
||||
<span>{$LL.configure.chords.NEW_CHORD()}</span>
|
||||
{/if}
|
||||
<ActionString display="keys" actions={editing ? [...pressedKeys].sort(compare) : chord?.actions ?? []} />
|
||||
<button class="icon add" on:click|stopPropagation={addSpecial}>add_circle</button>
|
||||
<sup>•</sup>
|
||||
</button>
|
||||
|
||||
@@ -74,7 +93,19 @@
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
|
||||
button {
|
||||
.add {
|
||||
font-size: 18px;
|
||||
margin-inline-start: 4px;
|
||||
height: 20px;
|
||||
opacity: 0;
|
||||
--icon-fill: 1;
|
||||
}
|
||||
|
||||
.chord:hover .add {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chord {
|
||||
position: relative;
|
||||
|
||||
display: inline-flex;
|
||||
@@ -88,7 +119,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
button::after {
|
||||
.chord::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
|
||||
@@ -13,7 +13,13 @@
|
||||
|
||||
function remove() {
|
||||
changes.update(changes => {
|
||||
changes.push({type: ChangeType.Chord, id: chord.id, actions: chord.actions, phrase: []})
|
||||
changes.push({
|
||||
type: ChangeType.Chord,
|
||||
id: chord.id,
|
||||
actions: chord.actions,
|
||||
phrase: chord.phrase,
|
||||
deleted: true,
|
||||
})
|
||||
return changes
|
||||
})
|
||||
}
|
||||
@@ -60,9 +66,9 @@
|
||||
<ChordPhraseEdit {chord} />
|
||||
</td>
|
||||
<td class="table-buttons">
|
||||
{#if chord.phrase.length !== 0}
|
||||
{#if !chord.deleted}
|
||||
<button transition:slide class="icon compact" on:click={remove}>delete</button>
|
||||
{:else if chord.phraseChanged}
|
||||
{:else}
|
||||
<button transition:slide class="icon compact" on:click={restore}>restore_from_trash</button>
|
||||
{/if}
|
||||
<button class="icon compact" class:disabled={chord.isApplied} on:click={restore}>undo</button>
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
import type {ChordInfo} from "$lib/undo-redo"
|
||||
import {scale} from "svelte/transition"
|
||||
import ActionString from "$lib/components/ActionString.svelte"
|
||||
import {selectAction} from "./action-selector"
|
||||
import {inputToAction} from "./input-converter"
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
import {get} from "svelte/store"
|
||||
|
||||
export let chord: ChordInfo
|
||||
|
||||
function keypress(event: KeyboardEvent) {
|
||||
if (event.key === "ArrowUp") {
|
||||
selectAction()
|
||||
addSpecial(event)
|
||||
} else if (event.key === "ArrowLeft") {
|
||||
moveCursor(cursorPosition - 1)
|
||||
} else if (event.key === "ArrowRight") {
|
||||
@@ -21,12 +25,12 @@
|
||||
moveCursor(cursorPosition - 1)
|
||||
} else if (event.key === "Delete") {
|
||||
deleteAction(cursorPosition)
|
||||
} else if (KEYMAP_IDS.has(event.key)) {
|
||||
insertAction(cursorPosition, KEYMAP_IDS.get(event.key)!.code)
|
||||
tick().then(() => moveCursor(cursorPosition + 1))
|
||||
} else if (specialKeycodes.has(event.key)) {
|
||||
insertAction(cursorPosition, specialKeycodes.get(event.key)!)
|
||||
tick().then(() => moveCursor(cursorPosition + 1))
|
||||
} else {
|
||||
const action = inputToAction(event, get(serialPort)?.device === "X")
|
||||
if (action !== undefined) {
|
||||
insertAction(cursorPosition, action)
|
||||
tick().then(() => moveCursor(cursorPosition + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,49 +81,15 @@
|
||||
moveCursor(i - 1)
|
||||
}
|
||||
|
||||
function selectAction() {
|
||||
const component = new ActionSelector({target: document.body})
|
||||
const dialog = document.querySelector("dialog > div") as HTMLDivElement
|
||||
const backdrop = document.querySelector("dialog") as HTMLDialogElement
|
||||
const dialogRect = dialog.getBoundingClientRect()
|
||||
const groupRect = button.getBoundingClientRect()
|
||||
|
||||
const scale = 0.5
|
||||
const dialogScale = `${1 - scale * (1 - groupRect.width / dialogRect.width)} ${
|
||||
1 - scale * (1 - groupRect.height / dialogRect.height)
|
||||
}`
|
||||
const dialogTranslate = `${scale * (groupRect.x - dialogRect.x)}px ${
|
||||
scale * (groupRect.y - dialogRect.y)
|
||||
}px`
|
||||
|
||||
const duration = 150
|
||||
const options = {duration, easing: "ease"}
|
||||
const dialogAnimation = dialog.animate(
|
||||
[
|
||||
{scale: dialogScale, translate: dialogTranslate},
|
||||
{translate: "0 0", scale: "1"},
|
||||
],
|
||||
options,
|
||||
function addSpecial(event: MouseEvent | KeyboardEvent) {
|
||||
selectAction(
|
||||
event,
|
||||
action => {
|
||||
insertAction(cursorPosition, action)
|
||||
tick().then(() => moveCursor(cursorPosition + 1))
|
||||
},
|
||||
() => box.focus(),
|
||||
)
|
||||
const backdropAnimation = backdrop.animate([{opacity: 0}, {opacity: 1}], options)
|
||||
|
||||
async function closed() {
|
||||
dialogAnimation.reverse()
|
||||
backdropAnimation.reverse()
|
||||
|
||||
await dialogAnimation.finished
|
||||
|
||||
component.$destroy()
|
||||
await tick()
|
||||
box.focus()
|
||||
}
|
||||
|
||||
component.$on("close", closed)
|
||||
component.$on("select", ({detail}) => {
|
||||
insertAction(cursorPosition, detail)
|
||||
tick().then(() => moveCursor(cursorPosition + 1))
|
||||
closed()
|
||||
})
|
||||
}
|
||||
|
||||
let button: HTMLButtonElement
|
||||
@@ -136,7 +106,7 @@
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
bind:this={box}
|
||||
class:edited={chord.phrase.length !== 0 && chord.phraseChanged}
|
||||
class:edited={!chord.deleted && chord.phraseChanged}
|
||||
on:focusin={() => (hasFocus = true)}
|
||||
on:focusout={event => {
|
||||
if (event.relatedTarget !== button) hasFocus = false
|
||||
@@ -144,7 +114,7 @@
|
||||
>
|
||||
{#if hasFocus}
|
||||
<div transition:scale class="cursor" style:translate="{cursorOffset}px 0">
|
||||
<button class="icon" bind:this={button} on:click={selectAction}>add</button>
|
||||
<button class="icon" bind:this={button} on:click={addSpecial}>add</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div />
|
||||
|
||||
50
src/routes/config/chords/action-selector.ts
Normal file
50
src/routes/config/chords/action-selector.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||
import {tick} from "svelte"
|
||||
|
||||
export function selectAction(
|
||||
event: MouseEvent | KeyboardEvent,
|
||||
select: (action: number) => void,
|
||||
dismissed?: () => void,
|
||||
) {
|
||||
const component = new ActionSelector({target: document.body})
|
||||
const dialog = document.querySelector("dialog > div") as HTMLDivElement
|
||||
const backdrop = document.querySelector("dialog") as HTMLDialogElement
|
||||
const dialogRect = dialog.getBoundingClientRect()
|
||||
const groupRect = (event.target as HTMLElement).getBoundingClientRect()
|
||||
|
||||
const scale = 0.5
|
||||
const dialogScale = `${1 - scale * (1 - groupRect.width / dialogRect.width)} ${
|
||||
1 - scale * (1 - groupRect.height / dialogRect.height)
|
||||
}`
|
||||
const dialogTranslate = `${scale * (groupRect.x - dialogRect.x)}px ${
|
||||
scale * (groupRect.y - dialogRect.y)
|
||||
}px`
|
||||
|
||||
const duration = 150
|
||||
const options = {duration, easing: "ease"}
|
||||
const dialogAnimation = dialog.animate(
|
||||
[
|
||||
{scale: dialogScale, translate: dialogTranslate},
|
||||
{translate: "0 0", scale: "1"},
|
||||
],
|
||||
options,
|
||||
)
|
||||
const backdropAnimation = backdrop.animate([{opacity: 0}, {opacity: 1}], options)
|
||||
|
||||
async function closed() {
|
||||
dialogAnimation.reverse()
|
||||
backdropAnimation.reverse()
|
||||
|
||||
await dialogAnimation.finished
|
||||
|
||||
component.$destroy()
|
||||
await tick()
|
||||
dismissed?.()
|
||||
}
|
||||
|
||||
component.$on("close", closed)
|
||||
component.$on("select", ({detail}) => {
|
||||
select(detail)
|
||||
closed()
|
||||
})
|
||||
}
|
||||
9
src/routes/config/chords/input-converter.ts
Normal file
9
src/routes/config/chords/input-converter.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {KEYMAP_IDS, KEYMAP_KEYCODES, specialKeycodes} from "$lib/serial/keymap-codes"
|
||||
|
||||
export function inputToAction(event: KeyboardEvent, useKeycodes?: boolean): number | undefined {
|
||||
if (useKeycodes) {
|
||||
return KEYMAP_KEYCODES.get(event.code)
|
||||
} else {
|
||||
return KEYMAP_IDS.get(event.key)?.code ?? specialKeycodes.get(event.key)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
onHidden(instance) {
|
||||
instance.destroy()
|
||||
},
|
||||
onDestroy(instance) {
|
||||
onDestroy() {
|
||||
shareComponent.$destroy()
|
||||
},
|
||||
}).show()
|
||||
@@ -49,6 +49,11 @@
|
||||
setContext("active-layer", writable(0))
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Layout Manager - CharaChorder Device Manager</title>
|
||||
<meta name="description" content="Edit your layout" />
|
||||
</svelte:head>
|
||||
|
||||
<svelte:window use:share={shareLayout} />
|
||||
|
||||
<section>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<script>
|
||||
import Action from "$lib/components/Action.svelte"
|
||||
import {popup} from "$lib/popup"
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
import {setting} from "$lib/setting"
|
||||
import ResetPopup from "./ResetPopup.svelte"
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Device Settings - CharaChorder Device Manager</title>
|
||||
<meta name="description" content="Change your device's settings" />
|
||||
</svelte:head>
|
||||
|
||||
{#if $serialPort}
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 41}} />Spurring</label></legend>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 0x41}} />Spurring</label></legend>
|
||||
<p>
|
||||
"Chording only" mode which tells your device to output chords on a press rather than a press &
|
||||
release. It also enables you to jump from one chord to another without releasing everything and can be
|
||||
@@ -14,117 +22,149 @@
|
||||
chording, but also takes away the flexibility of character entry.
|
||||
</p>
|
||||
<p>Spurring also helps new users learn how to chord by eliminating the need to focus on timing.</p>
|
||||
<p>Spurring is toggled by chording both of the 'mirror' keys together.</p>
|
||||
<p>
|
||||
Spurring is toggled by chording <Action display="keys" action={540} /> and <Action
|
||||
display="keys"
|
||||
action={542}
|
||||
/> together.
|
||||
</p>
|
||||
<label
|
||||
>Character Counter Timeout<span class="unit"
|
||||
><input type="number" step="0.001" min="0" max="240" use:setting={{id: 43, scale: 0.001}} />s</span
|
||||
><input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
max="240"
|
||||
use:setting={{id: 0x43, scale: 0.001}}
|
||||
/>s</span
|
||||
></label
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 51}} />Arpeggiates</label></legend>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 0x51}} />Arpeggiates</label></legend>
|
||||
<p>
|
||||
A quick, single key press and release used to indicate a suffix, prefix, or modifier to be associated
|
||||
with a chord.
|
||||
</p>
|
||||
<p>The following keys have special behavior when arpeggiates are enabled:</p>
|
||||
<ul>
|
||||
<li><kbd>,</kbd>, <kbd>;</kbd> and <kbd>:</kbd> will be placed before the auto-inserted space</li>
|
||||
<li>
|
||||
<kbd>.</kbd>, <kbd>?</kbd> and <kbd>!</kbd> will be placed before the auto-inserted space and capitalize
|
||||
the next word
|
||||
<Action display="keys" action={44} />, <Action display="keys" action={59} /> and <Action
|
||||
display="keys"
|
||||
action={58}
|
||||
/> will be placed before the auto-inserted space
|
||||
</li>
|
||||
<li>
|
||||
<Action display="keys" action={46} />, <Action display="keys" action={63} /> and <Action
|
||||
display="keys"
|
||||
action={33}
|
||||
/> will be placed before the auto-inserted space and capitalize the next word
|
||||
</li>
|
||||
<li>
|
||||
<Action display="keys" action={45} /> and <Action display="keys" action={47} /> will replace the auto-inserted
|
||||
space
|
||||
</li>
|
||||
<li><kbd>-</kbd> will replace the auto-inserted space</li>
|
||||
</ul>
|
||||
<label
|
||||
>Timeout After Chord<span class="unit"><input type="number" step="1" use:setting={{id: 54}} />ms</span
|
||||
>Timeout After Chord<span class="unit"
|
||||
><input type="number" step="1" use:setting={{id: 0x54}} />ms</span
|
||||
></label
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Chord Modifiers</legend>
|
||||
<p>
|
||||
Chord modifiers change a chord when held with the chord or when pressed after (arpeggiated), <b
|
||||
>provided that arpeggiates are enabled.</b
|
||||
>
|
||||
</p>
|
||||
<ul>
|
||||
<li><Action display="keys" action={513} /> Capitalizes the first letter of a chord</li>
|
||||
<li><Action display="keys" action={540} /> Present Tense (supported words only)</li>
|
||||
<li><Action display="keys" action={542} /> Plural (supported words only)</li>
|
||||
<li><Action display="keys" action={550} /> Past Tense (supported words only)</li>
|
||||
<li><Action display="keys" action={551} /> Comparative (supported words only)</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Character Entry</legend>
|
||||
{#if $serialPort.device === "LITE"}
|
||||
<label>Swap Keymap 0 and 1<input type="checkbox" use:setting={{id: 13}} /></label>
|
||||
<label>Swap Keymap 0 and 1<input type="checkbox" use:setting={{id: 0x13}} /></label>
|
||||
{/if}
|
||||
<label>
|
||||
Character Entry (chentry)
|
||||
<input type="checkbox" use:setting={{id: 12}} />
|
||||
<input type="checkbox" use:setting={{id: 0x12}} />
|
||||
</label>
|
||||
<label>
|
||||
Key Scan Rate
|
||||
<span class="unit"><input type="number" use:setting={{id: 14, inverse: 1000}} />Hz</span></label
|
||||
<span class="unit"><input type="number" use:setting={{id: 0x14, inverse: 1000}} />Hz</span></label
|
||||
>
|
||||
<label>
|
||||
Key Debounce Press<span class="unit"><input type="number" use:setting={{id: 15}} />ms</span></label
|
||||
Key Debounce Press<span class="unit"><input type="number" use:setting={{id: 0x15}} />ms</span></label
|
||||
>
|
||||
<label
|
||||
>Key Debounce Release<span class="unit"><input type="number" use:setting={{id: 16}} />ms</span></label
|
||||
>Key Debounce Release<span class="unit"><input type="number" use:setting={{id: 0x16}} />ms</span
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Output Character Delay<span class="unit"><input type="number" use:setting={{id: 17}} />µs</span
|
||||
>Output Character Delay<span class="unit"><input type="number" use:setting={{id: 0x17}} />µs</span
|
||||
></label
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 21}} />Mouse</label></legend>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 0x21}} />Mouse</label></legend>
|
||||
<label
|
||||
>Mouse Speed<input type="number" use:setting={{id: 22}} /><input
|
||||
>Mouse Speed<input type="number" use:setting={{id: 0x22}} /><input
|
||||
type="number"
|
||||
use:setting={{id: 23}}
|
||||
use:setting={{id: 0x23}}
|
||||
/></label
|
||||
>
|
||||
<label>Scroll Speed<input type="number" use:setting={{id: 25}} /></label>
|
||||
<label title="Bounces mouse by 1px every 60s if enabled"
|
||||
>Active Mouse<input type="checkbox" use:setting={{id: 24}} /></label
|
||||
<label>Scroll Speed<input type="number" use:setting={{id: 0x25}} /></label>
|
||||
<label>
|
||||
<span>
|
||||
Active Mouse
|
||||
<p>Bounces mouse by 1px every 60s if enabled</p></span
|
||||
>
|
||||
<input type="checkbox" use:setting={{id: 0x24}} /></label
|
||||
>
|
||||
<label
|
||||
>Poll Rate<span class="unit"><input type="number" use:setting={{id: 26, inverse: 1000}} />Hz</span
|
||||
>Poll Rate<span class="unit"><input type="number" use:setting={{id: 0x26, inverse: 1000}} />Hz</span
|
||||
></label
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 31}} />Chording</label></legend>
|
||||
<legend><label><input type="checkbox" use:setting={{id: 0x31}} />Chording</label></legend>
|
||||
<label
|
||||
>Character Timeout <span class="unit"
|
||||
><input type="number" min="0" max="25.5" step="0.1" use:setting={{id: 33, scale: 0.001}} />s</span
|
||||
>Auto-delete Timeout <span class="unit"
|
||||
><input type="number" min="0" max="25500" step="10" use:setting={{id: 0x33}} />ms</span
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Detection Tolerance<span class="unit"
|
||||
><input type="number" min="1" max="50" step="1" use:setting={{id: 34}} />ms</span
|
||||
>Press Tolerance<span class="unit"
|
||||
><input type="number" min="1" max="50" step="1" use:setting={{id: 0x34}} />ms</span
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Release Tolerance<span class="unit"
|
||||
><input type="number" min="1" max="50" step="1" use:setting={{id: 35}} />ms</span
|
||||
><input type="number" min="1" max="50" step="1" use:setting={{id: 0x35}} />ms</span
|
||||
></label
|
||||
>
|
||||
<label>Compound Chording<input type="checkbox" use:setting={{id: 61}} /></label>
|
||||
<label>Compound Chording<input type="checkbox" use:setting={{id: 0x61}} /></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Device</legend>
|
||||
<label>Boot message<input type="checkbox" use:setting={{id: 93}} /></label>
|
||||
<label>GTM Realtime Feedback<input type="checkbox" use:setting={{id: 92}} /></label>
|
||||
<label>
|
||||
Operating System
|
||||
<select>
|
||||
<option value="0">Windows</option>
|
||||
<option value="1">MacOS</option>
|
||||
<option value="2">Linux</option>
|
||||
<option value="3">iOS</option>
|
||||
<option value="4">Android</option>
|
||||
<option value="255">Unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
<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"}
|
||||
<!-- TODO -->
|
||||
<fieldset>
|
||||
<legend><label><input type="checkbox" />RGB</label></legend>
|
||||
<label>Brightness<input type="range" min="0" max="50" step="1" /></label>
|
||||
@@ -147,6 +187,14 @@
|
||||
padding-block-end: 48px;
|
||||
}
|
||||
|
||||
button.outline {
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 8px;
|
||||
height: 2em;
|
||||
margin-block: 2em;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
legend,
|
||||
legend > label {
|
||||
font-size: 24px;
|
||||
@@ -168,6 +216,7 @@
|
||||
}
|
||||
|
||||
> label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
@@ -177,6 +226,20 @@
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
> input[type="number"] {
|
||||
border-radius: 16px 4px 4px 16px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
|
||||
&:last-child:not(:only-child) {
|
||||
border-radius: 4px 16px 16px 4px;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(input[type="number"]) {
|
||||
cursor: text;
|
||||
|
||||
@@ -234,11 +297,23 @@
|
||||
ul,
|
||||
p {
|
||||
font-size: 10px;
|
||||
|
||||
:global(kbd) {
|
||||
font-size: 12px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line
|
||||
form label:has(:global(.pending-changes)) {
|
||||
color: var(--md-sys-color-tertiary);
|
||||
label:global(:has(.pending-changes)) {
|
||||
color: var(--md-sys-color-primary);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.25em;
|
||||
content: "•";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
38
src/routes/config/settings/ConfirmChallenge.svelte
Normal file
38
src/routes/config/settings/ConfirmChallenge.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
import {createEventDispatcher} from "svelte"
|
||||
|
||||
export let challenge: string
|
||||
|
||||
let challengeInput = ""
|
||||
$: challengeString = `${challenge} ${$serialPort!.device}`
|
||||
$: isValid = challengeInput === challengeString
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<h3>Type the following to confim the action</h3>
|
||||
|
||||
<p>{challengeString}</p>
|
||||
<input type="text" bind:value={challengeInput} placeholder={challengeString} />
|
||||
|
||||
<button disabled={!isValid} on:click={() => dispatch("confirm")}>Confirm {challenge}</button>
|
||||
|
||||
<style lang="scss">
|
||||
input[type="text"] {
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid currentcolor;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-sys-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
</style>
|
||||
42
src/routes/config/settings/ResetPopup.svelte
Normal file
42
src/routes/config/settings/ResetPopup.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import {confirmChallenge} from "./confirm-challenge"
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
|
||||
const options = [
|
||||
[["FACTORY", "Factory Reset"]],
|
||||
[
|
||||
["PARAMS", "Reset Settings"],
|
||||
["KEYMAPS", "Reset Layout"],
|
||||
["CLEARCML", "Clear Chords"],
|
||||
],
|
||||
[
|
||||
["STARTER", "Add starter chords"],
|
||||
["FUNC", "Add functional chords"],
|
||||
],
|
||||
] as const
|
||||
</script>
|
||||
|
||||
<h3>Reset Device</h3>
|
||||
{#each options as category, i}
|
||||
{#if i > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
{#each category as [command, description]}
|
||||
<button
|
||||
class="error"
|
||||
use:confirmChallenge={{
|
||||
onConfirm() {
|
||||
$serialPort?.reset(command)
|
||||
$serialPort = undefined
|
||||
},
|
||||
challenge: description,
|
||||
}}>{description}...</button
|
||||
>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
<style lang="scss">
|
||||
hr {
|
||||
opacity: 0.25;
|
||||
}
|
||||
</style>
|
||||
37
src/routes/config/settings/confirm-challenge.ts
Normal file
37
src/routes/config/settings/confirm-challenge.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type {Action} from "svelte/action"
|
||||
import ConfirmChallenge from "./ConfirmChallenge.svelte"
|
||||
import tippy from "tippy.js"
|
||||
|
||||
export const confirmChallenge: Action<HTMLElement, {onConfirm: () => void; challenge: string}> = (
|
||||
node,
|
||||
{onConfirm, challenge},
|
||||
) => {
|
||||
let component: ConfirmChallenge | undefined
|
||||
let target: HTMLElement | undefined
|
||||
const edit = tippy(node, {
|
||||
interactive: true,
|
||||
trigger: "click",
|
||||
onShow(instance) {
|
||||
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()
|
||||
})
|
||||
}
|
||||
},
|
||||
onHidden() {
|
||||
component?.$destroy()
|
||||
target?.classList.remove("active")
|
||||
component = undefined
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
edit.destroy()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -139,13 +139,7 @@
|
||||
<div class="editor-root" bind:this={editor} />
|
||||
</section>
|
||||
|
||||
<iframe
|
||||
aria-hidden="true"
|
||||
title="code sandbox"
|
||||
bind:this={frame}
|
||||
src="/sandbox.html"
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
<iframe aria-hidden="true" title="code sandbox" bind:this={frame} src="/sandbox/" sandbox="allow-scripts" />
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
|
||||
0
src/routes/sandbox/+layout@.svelte
Normal file
0
src/routes/sandbox/+layout@.svelte
Normal file
43
src/routes/sandbox/+page.svelte
Normal file
43
src/routes/sandbox/+page.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
let ongoingRequest
|
||||
let resolveRequest
|
||||
let source
|
||||
async function post(channel, args) {
|
||||
while (ongoingRequest) {
|
||||
await ongoingRequest
|
||||
}
|
||||
ongoingRequest = new Promise(resolve => {
|
||||
resolveRequest = resolve
|
||||
source.postMessage([channel, args], "*")
|
||||
})
|
||||
ongoingRequest.then(() => {
|
||||
ongoingRequest = undefined
|
||||
})
|
||||
return ongoingRequest
|
||||
}
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
if ("response" in event.data) {
|
||||
resolveRequest(event.data.response)
|
||||
} else {
|
||||
source = event.source
|
||||
|
||||
var Action = event.data.actionCodes
|
||||
Object.assign(
|
||||
Action,
|
||||
Object.fromEntries(
|
||||
Object.values(event.data.actionCodes)
|
||||
.filter(it => !!it.id)
|
||||
.map(it => [it.id, it]),
|
||||
),
|
||||
)
|
||||
|
||||
var Chara = {}
|
||||
for (const fn of event.data.charaChannels) {
|
||||
Chara[fn] = (...args) => post(fn, args)
|
||||
}
|
||||
|
||||
eval(`(async function(){${event.data.script}})()`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
7
src/routes/update-guide/+page.svelte
Normal file
7
src/routes/update-guide/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {LL} from "../../i18n/i18n-svelte"
|
||||
</script>
|
||||
|
||||
<h1>{$LL.update.TITLE()}</h1>
|
||||
|
||||
<a href="https://github.com/CharaChorder/CCOS-firmware/blob/main/CHANGELOG.md" target="_blank">Changelog</a>
|
||||
6
src/tools/icons-config.d.ts
vendored
Normal file
6
src/tools/icons-config.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface IconsConfig {
|
||||
codePoints: Record<string, string>
|
||||
inputPath: string
|
||||
outputPath: string
|
||||
icons: string[]
|
||||
}
|
||||
@@ -16,11 +16,15 @@
|
||||
import {openSync} from "fontkit"
|
||||
import {exec} from "child_process"
|
||||
import config from "../../icons.config.js"
|
||||
import {statSync, existsSync} from "fs"
|
||||
import {statSync} from "fs"
|
||||
import {readFile} from "fs/promises"
|
||||
import {glob} from "glob"
|
||||
|
||||
async function run(command: string[] | string): Promise<string> {
|
||||
/**
|
||||
* @param {string[] | string} command
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function run(command) {
|
||||
const fullCommand = Array.isArray(command) ? command.join(" ") : command
|
||||
console.log(`>> ${fullCommand}`)
|
||||
|
||||
@@ -51,7 +55,8 @@ const font = openSync(config.inputPath)
|
||||
|
||||
const glyphs = ["5f-7a", "30-39"]
|
||||
for (const icon of icons) {
|
||||
const iconGlyphs: Array<{id: string}> = font.layout(icon).glyphs
|
||||
/** @type {Array<{id: string}>} */
|
||||
const iconGlyphs = font.layout(icon).glyphs
|
||||
if (iconGlyphs.length === 0) {
|
||||
console.error(`${icon} not found in font. Typo?`)
|
||||
process.exit(-1)
|
||||
@@ -62,15 +67,13 @@ for (const icon of icons) {
|
||||
.flatMap(it => [...it])
|
||||
.map(it => it.codePointAt(0).toString(16))
|
||||
|
||||
if (codePoints.length === 0) {
|
||||
const codePoint = config.codePoints[icon]
|
||||
if (config.codePoints?.[icon]) {
|
||||
glyphs.push(config.codePoints[icon])
|
||||
} else {
|
||||
console.log()
|
||||
console.error(`${icon} code point could not be determined. Add it to config.codePoints.`)
|
||||
process.exit(-1)
|
||||
}
|
||||
const codePoint = config.codePoints[icon]
|
||||
if (codePoint) {
|
||||
glyphs.push(codePoint)
|
||||
} else if (codePoints.length === 0) {
|
||||
console.log()
|
||||
console.error(`${icon} code point could not be determined. Add it to config.codePoints.`)
|
||||
process.exit(-1)
|
||||
}
|
||||
|
||||
glyphs.push(...codePoints)
|
||||
@@ -101,8 +104,9 @@ console.log(
|
||||
|
||||
/**
|
||||
* Bytes to respective units
|
||||
* @param {number} value
|
||||
*/
|
||||
function toByteUnit(value: number) {
|
||||
function toByteUnit(value) {
|
||||
if (value < 1024) {
|
||||
return `${value}B`
|
||||
} else if (value < 1024 * 1024) {
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,5 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<svg version="1.1" width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="72" cy="72" r="44" fill="black" />
|
||||
<text x="72" y="82" font-size="36" font-weight="900" font-family="monospace" text-anchor="middle" dominant-baseline="middle" fill="white">i/o</text>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="inner">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle cx="256" cy="256" r="64" fill="black"/>
|
||||
<rect x="256" y="192" width="256" height="128" fill="black"/>
|
||||
</mask>
|
||||
<mask id="outer">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
|
||||
<circle cx="256" cy="256" r="160" fill="black" />
|
||||
</mask>
|
||||
|
||||
<circle cx="256" cy="256" r="256" fill="black"/>
|
||||
|
||||
<g mask="url(#inner)">
|
||||
<circle cx="256" cy="256" r="226" fill="white" mask="url(#outer)" />
|
||||
<circle cx="256" cy="256" r="146" fill="white" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 352 B After Width: | Height: | Size: 685 B |
@@ -1,49 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>iFrame Sandbox</title>
|
||||
<script>
|
||||
let ongoingRequest
|
||||
let resolveRequest
|
||||
let source
|
||||
async function post(channel, args) {
|
||||
while (ongoingRequest) {
|
||||
await ongoingRequest
|
||||
}
|
||||
ongoingRequest = new Promise(resolve => {
|
||||
resolveRequest = resolve
|
||||
source.postMessage([channel, args], "*")
|
||||
})
|
||||
ongoingRequest.then(() => {
|
||||
ongoingRequest = undefined
|
||||
})
|
||||
return ongoingRequest
|
||||
}
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
if ("response" in event.data) {
|
||||
resolveRequest(event.data.response)
|
||||
} else {
|
||||
source = event.source
|
||||
|
||||
var Action = event.data.actionCodes
|
||||
Object.assign(
|
||||
Action,
|
||||
Object.fromEntries(
|
||||
Object.values(event.data.actionCodes)
|
||||
.filter(it => !!it.id)
|
||||
.map(it => [it.id, it]),
|
||||
),
|
||||
)
|
||||
|
||||
var Chara = {}
|
||||
for (const fn of event.data.charaChannels) {
|
||||
Chara[fn] = (...args) => post(fn, args)
|
||||
}
|
||||
|
||||
eval(`(async function(){${event.data.script}})()`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
@@ -10,7 +10,7 @@ const {version} = JSON.parse(await readFile(fileURLToPath(new URL("package.json"
|
||||
const config = {
|
||||
preprocess: [preprocess({postcss: {plugins: autoprefixer()}})],
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
adapter: adapter({fallback: "404.html"}),
|
||||
version: {
|
||||
name: version,
|
||||
},
|
||||
|
||||
@@ -9,17 +9,20 @@ import {fileURLToPath} from "url"
|
||||
|
||||
const isTauri = "TAURI_FAMILY" in process.env
|
||||
console.info(isTauri ? "Building for Tauri" : "Building for PWA")
|
||||
const {homepage, bugs} = JSON.parse(
|
||||
const {homepage, bugs, repository} = JSON.parse(
|
||||
await readFile(fileURLToPath(new URL("package.json", import.meta.url)), "utf8"),
|
||||
)
|
||||
|
||||
process.env.VITE_HOMEPAGE_URL = homepage
|
||||
process.env.VITE_HOMEPAGE_URL = repository.url.replace(/\.git$/, "")
|
||||
process.env.VITE_DOCS_URL = homepage
|
||||
process.env.VITE_BUGS_URL = bugs.url
|
||||
process.env.VITE_LEARN_URL = "https://www.iq-eq.io/"
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
// we rely on the serial api, so just chrome is fine
|
||||
target: ["chrome114", "safari16"],
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: isTauri ? [/virtual:pwa.*/] : [],
|
||||
},
|
||||
@@ -39,11 +42,12 @@ export default defineConfig({
|
||||
base: "/",
|
||||
includeAssets: ["favicon.png"],
|
||||
workbox: {
|
||||
globPatterns: ["**/*.{js,css,html,woff2,json,csv,png,svg}"],
|
||||
// https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#globpatterns
|
||||
globPatterns: ["client/**/*.{js,map,css,woff2,csv,png,svg}", "prerendered/**/*.html"],
|
||||
},
|
||||
manifest: {
|
||||
name: "dot i/o",
|
||||
id: "dot_io_v2",
|
||||
name: "CharaChorder Device Manager",
|
||||
id: "charchorder-device-manager",
|
||||
theme_color: themeColor,
|
||||
icons: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user