80 Commits

Author SHA1 Message Date
5fa4b1fd09 1.2.0 2024-01-03 14:59:12 +01:00
f585a0ebda fix: disable Tauri publish for now 2024-01-03 14:50:51 +01:00
a48e2b5a16 fix: keyinfo missing display type prop 2024-01-03 14:21:37 +01:00
fd612eda1d fix: dynamic mappings are not displayed 2024-01-03 14:21:13 +01:00
a1fe6f7110 feat: periodically update os-layout in the background
fix: remove dead code in layout detection
fixes #78
resolves #79
2024-01-03 13:55:50 +01:00
0e57e810e0 feat: change icons 2024-01-03 01:26:39 +01:00
a15d5dde38 feat: inform user when save action failed
fixes #67
2023-12-30 16:04:16 +01:00
560206129e feat: add meta for config pages 2023-12-30 16:03:31 +01:00
cb7c70dac1 refactor: flatten visual key positioning system
fixes #74
fixes #43
2023-12-30 15:50:48 +01:00
edabf8ec84 fix: settings without min/max parse as 0
fixes #75
2023-12-30 12:50:46 +01:00
f2f61f32f2 feat: add reset options
resolves #70
2023-12-29 15:04:33 +01:00
a3857843d6 feat: use keycodes on CCX
resolves #71
2023-12-29 13:48:34 +01:00
c1b1068c4b fix: settings.yml missing hex prefix
feat: add direnv config
2023-12-29 13:23:41 +01:00
2411dd2bea feat: show dynamic key maps in layout view 2023-12-22 12:51:23 +01:00
7911904906 Revert "refactor: remove outdated files"
This reverts commit 84b22e0006.
2023-12-21 23:55:29 +01:00
630687de80 fix: use hexadecimal for settings 2023-12-21 23:49:55 +01:00
84b22e0006 refactor: remove outdated files 2023-12-21 23:39:23 +01:00
dd070c8856 feat: include source maps in pwa 2023-12-21 21:31:43 +01:00
6872cd0554 fix: build 2023-12-21 21:24:12 +01:00
628007af23 fix: align settings wording with gtm 2023-12-21 21:23:00 +01:00
19fad84357 fix: svg invalid 2023-12-21 21:20:26 +01:00
f172318a78 feat: update logo and favicon 2023-12-21 21:19:17 +01:00
c2e3850082 fix: maybe fix cloudflare pages loop 2023-12-21 20:47:06 +01:00
7a5a4eb434 feat: update pwa icon 2023-12-21 20:46:12 +01:00
c878311f62 feat: add web manifest to site meta 2023-12-21 20:28:47 +01:00
fb3fb246e9 fix: remove json files from pwa glob pattern 2023-12-21 20:19:55 +01:00
b4e4ca84a4 fix: pwa tries to include build-only files 2023-12-21 20:05:32 +01:00
c1b1544256 fix: mouse & scroll-speed options are unstyled
resolves #44
2023-12-21 19:42:37 +01:00
03dd528465 feat: add ability to add special actions to chord inputs
resolves #10
2023-12-21 19:19:47 +01:00
81af9f2e82 feat: add min/max enforcement to device settings
resolves #6
2023-12-21 18:31:50 +01:00
6bb42429e5 feat: add change indicator on settings page 2023-12-21 18:23:47 +01:00
d07751a944 fix: remove OS setting
resolves #61
2023-12-21 18:12:37 +01:00
8867030ede fix: PWA fixes 2023-12-21 18:08:16 +01:00
faaa6dd5be fix: keyboard action variant wrong 2023-12-21 16:11:02 +01:00
43cf13094e fix: "p" key missing for ccx and linked with "o" key 2023-12-20 20:01:28 +01:00
ed523628ff feat: try typing field in chords section
resolves #68
fix: "No Results" not translated
2023-12-18 18:42:08 +01:00
98b451eec9 1.1.0 2023-12-17 00:33:11 +01:00
6e37dc198f feat: rework character timeout setting 2023-12-16 18:13:02 +01:00
e319b1bfaf fix: swap top/bottom thumb labels
fixes #65
2023-12-16 15:27:34 +01:00
eb33b64100 feat: reject new chords that override another chord 2023-12-16 15:24:51 +01:00
766bc44a85 feat: do not use empty phrase for deleted chords 2023-12-16 15:20:44 +01:00
b679aa377a fix: key text showing focus outline
fix: layout selectable
2023-12-16 13:11:52 +01:00
ea3192d4e6 feat: add links to docs and dotio 2023-12-16 12:46:20 +01:00
256daec412 feat: chord modifier hints 2023-12-15 19:54:31 +01:00
29a07133d1 fix: deadlock 2023-12-15 16:59:06 +01:00
c3bd8431e5 feat: debounce connection suspension 2023-12-15 16:43:56 +01:00
c8e04ed6cc feat: auto-reconnect after reboot 2023-12-12 18:37:40 +01:00
d98653995b feat: bootloader warning
refactor: reword linux premission warning
2023-12-12 18:30:20 +01:00
3dd9611ebf feat: Linux permission guide 2023-12-12 18:03:34 +01:00
9d9360375b 1.0.0 2023-12-08 23:52:33 +01:00
d683c8c70c fix: action selector shows next item every time 2023-12-08 23:21:21 +01:00
d8d430f333 fix: browser warning referencing a non-existent app 2023-12-08 23:12:52 +01:00
fe850f47ec feat: add info for current and after next save action 2023-12-08 23:04:38 +01:00
f9a63a8724 fix: warn users if no device is connected
fix: can't backup without a device
2023-12-08 22:55:33 +01:00
af01426f43 fix: action tooltips not updating 2023-12-08 22:46:01 +01:00
9d7cefb3b4 fix: add ui when no device is connected
fixes #60
2023-12-08 22:38:46 +01:00
f44e5a79de fix: action selector search bar color
fixes #39
2023-12-08 22:30:03 +01:00
8b2e92c124 feat: add icons to unassigned cc1 3D keys
fixes #58
2023-12-08 22:26:32 +01:00
f758be91a9 fix: build 2023-12-08 22:16:07 +01:00
bf4c86e698 fix: legacy chords with commas and spaces 2023-12-08 22:10:49 +01:00
50a09d2008 fix: PWA not working 2023-12-08 22:02:48 +01:00
3c1a4de4a7 fix: chord page overlapping
fixes #57, fixes #56
fix: handle trailing spaces in lecacy chord files
2023-12-08 21:59:08 +01:00
8cbdf1393f fix: chord files not detected properly
feat: alert on unknown backups
2023-12-08 21:14:37 +01:00
1ccb17f053 fix: allow trailing linebreak for legacy layouts 2023-12-08 21:04:18 +01:00
532dc70fe2 feat: ccx layout 2023-12-08 14:49:07 +01:00
d5893013f9 feat: ccx layout maybe 2023-12-08 14:13:31 +01:00
80308cad73 feat: ccx layout (hopefully) 2023-12-07 21:52:04 +01:00
2d59bd016f feat: ccx row 2 2023-12-07 21:40:27 +01:00
298de49257 test ccx row 2023-12-07 21:36:31 +01:00
3a62864a41 feat: ccx key count 2023-12-07 21:29:26 +01:00
109095e35e feat: re-introduce background sync 2023-12-07 19:51:23 +01:00
2dd6f39ac6 fix: reword alt-code warnings
fixes #11
2023-12-07 19:36:10 +01:00
b0f653e73b fix: weird input behaviour on setting changes 2023-12-07 19:27:29 +01:00
d552fb9220 fix: only import settings that already exist 2023-12-07 19:06:49 +01:00
77339620e6 fix: full backups fail because of invalid setting IDs 2023-12-07 18:58:40 +01:00
846183bbb1 feat: compound chording actions 2023-12-06 01:19:01 +01:00
1d53f6df7a fix: crash with missing action info in chords 2023-12-06 00:31:08 +01:00
58d13a4107 feat: enable source maps in production builds 2023-12-05 17:29:15 +01:00
f7d99d8d7b feat: dynamic keymap prototype 2023-12-03 00:01:51 +01:00
d9dd003b01 feat: show warnings about shift and alt-code macros
resolves #38
2023-12-02 23:26:04 +01:00
71 changed files with 1486 additions and 953 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -1,8 +1,8 @@
name: "publish"
name: "publish desktop apps"
on:
push:
tags:
- "v*"
- "desktop-app-v*"
workflow_dispatch:
jobs:

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ node_modules
/package
.env
.env.*
.direnv
!.env.example
venv
vite.config.js.timestamp-*

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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
View File

@@ -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 {

View File

@@ -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",

View File

@@ -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",

View 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

View File

@@ -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

View File

@@ -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"

View File

@@ -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.

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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;

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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)
}

View File

@@ -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}`)
}
/**

View File

@@ -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
View 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()
}

View File

@@ -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()
},
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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()}}

View File

@@ -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))) {

View File

@@ -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>

View File

@@ -0,0 +1,6 @@
{
"CHARACHORDER ONE M0": {
"latest": "1.1.3",
"next": null
}
}

View File

@@ -1 +0,0 @@
<h1>Layout Bootcamp</h1>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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 />

View 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()
})
}

View 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)
}
}

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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()
},
}
}

View File

@@ -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 {

View File

View 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>

View 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
View File

@@ -0,0 +1,6 @@
export interface IconsConfig {
codePoints: Record<string, string>
inputPath: string
outputPath: string
icons: string[]
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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,
},

View File

@@ -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: [
{