mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-03 08:32:52 +00:00
Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f322435c41
|
|||
|
587375e654
|
|||
|
0500a723de
|
|||
|
26dcc56aca
|
|||
|
20b65813bf
|
|||
|
87b23c04b1
|
|||
|
8b2bc6d109
|
|||
|
19cf0b26b3
|
|||
|
3e72dd3cb8
|
|||
|
a40daefbad
|
|||
|
77d4a90519
|
|||
|
c9a031a1fd
|
|||
|
254a0c1aec
|
|||
|
bd75012cf1
|
|||
|
4b738bb340
|
|||
|
3af65106bf
|
|||
|
8087d10d5a
|
|||
|
2782966505
|
|||
|
5b6d369101
|
|||
|
b423d1c661
|
|||
|
92a3c6012f
|
|||
|
8ec11c7ec9
|
|||
|
5c8eb1d19f
|
|||
|
91a044bbba
|
|||
|
1a6c85a361
|
|||
|
ecef11ac2d
|
|||
|
a23af9ba9d
|
|||
|
93849f250f
|
|||
|
33890b0aa8
|
|||
|
6f925de1af
|
|||
|
d45fe43f17
|
|||
|
59788f059d
|
|||
|
2808973ad0
|
|||
|
bef51d2a7d
|
|||
|
854ab6d3be
|
|||
|
86ec8651b6
|
|||
|
4e4bff02a0
|
|||
|
5d4dbc7e2a
|
|||
|
dfd1c0bcbd
|
|||
|
6ac2cd1993
|
|||
|
7256dc50d4
|
|||
|
f0ad19e6c2
|
|||
|
9022a09b4c
|
|||
|
7e3e61afd7
|
|||
|
08f594d164
|
|||
|
046595b51f
|
|||
|
fbc5303690
|
|||
|
ad41d39bfb
|
|||
|
6faaa18b3b
|
|||
|
6ab6959129
|
|||
|
44d89d3f35
|
|||
|
eaf0adaf01
|
|||
|
5b6a5ea36d
|
|||
|
14cbb5553b
|
|||
|
|
8ed72fe958 | ||
|
06b83f79ef
|
|||
|
5fa4b1fd09
|
|||
|
f585a0ebda
|
|||
|
a48e2b5a16
|
|||
|
fd612eda1d
|
|||
|
a1fe6f7110
|
|||
|
0e57e810e0
|
|||
|
a15d5dde38
|
|||
|
560206129e
|
|||
|
cb7c70dac1
|
|||
|
edabf8ec84
|
|||
|
f2f61f32f2
|
|||
|
a3857843d6
|
|||
|
c1b1068c4b
|
|||
|
2411dd2bea
|
|||
|
7911904906
|
|||
|
630687de80
|
|||
|
84b22e0006
|
|||
|
dd070c8856
|
|||
|
6872cd0554
|
|||
|
628007af23
|
|||
|
19fad84357
|
|||
|
f172318a78
|
|||
|
c2e3850082
|
|||
|
7a5a4eb434
|
|||
|
c878311f62
|
|||
|
fb3fb246e9
|
|||
|
b4e4ca84a4
|
|||
|
c1b1544256
|
|||
|
03dd528465
|
|||
|
81af9f2e82
|
|||
|
6bb42429e5
|
|||
|
d07751a944
|
|||
|
8867030ede
|
|||
|
faaa6dd5be
|
|||
|
43cf13094e
|
|||
|
ed523628ff
|
|||
|
98b451eec9
|
|||
|
6e37dc198f
|
|||
|
e319b1bfaf
|
|||
|
eb33b64100
|
|||
|
766bc44a85
|
|||
|
b679aa377a
|
|||
|
ea3192d4e6
|
|||
|
256daec412
|
|||
|
29a07133d1
|
|||
|
c3bd8431e5
|
|||
|
c8e04ed6cc
|
|||
|
d98653995b
|
|||
|
3dd9611ebf
|
|||
|
9d9360375b
|
|||
|
d683c8c70c
|
|||
|
d8d430f333
|
|||
|
fe850f47ec
|
|||
|
f9a63a8724
|
|||
|
af01426f43
|
|||
|
9d7cefb3b4
|
|||
|
f44e5a79de
|
|||
|
8b2e92c124
|
|||
|
f758be91a9
|
|||
|
bf4c86e698
|
|||
|
50a09d2008
|
|||
|
3c1a4de4a7
|
|||
|
8cbdf1393f
|
|||
|
1ccb17f053
|
|||
|
532dc70fe2
|
|||
|
d5893013f9
|
|||
|
80308cad73
|
|||
|
2d59bd016f
|
|||
|
298de49257
|
|||
|
3a62864a41
|
|||
|
109095e35e
|
|||
|
2dd6f39ac6
|
|||
|
b0f653e73b
|
|||
|
d552fb9220
|
|||
|
77339620e6
|
|||
|
846183bbb1
|
|||
|
1d53f6df7a
|
|||
|
58d13a4107
|
|||
|
f7d99d8d7b
|
|||
|
d9dd003b01
|
|||
|
dc798d2b9f
|
|||
|
c2ec460c8c
|
|||
|
c51bcc8ff0
|
|||
|
63b7f8ab18
|
|||
|
eaf8028538
|
|||
|
2ad0ef3b6d
|
|||
|
20705de069
|
|||
|
64b519d5b1
|
|||
|
fb490b3db6
|
|||
|
c37ae7da7b
|
|||
|
5c06c2206c
|
|||
|
f9cdf70bdb
|
|||
|
3a6483aa61
|
|||
|
|
018c7a5eac | ||
|
f73b8c1453
|
|||
|
e38d952e1d
|
|||
|
8e5692ca59
|
|||
|
a0fe925ea9
|
|||
|
e84470d577
|
|||
|
683561dc06
|
|||
|
2fd2dad6f7
|
|||
|
e2f9f87b13
|
|||
|
623d895aea
|
|||
|
561300de64
|
|||
|
c5d9defc9d
|
|||
|
acd58646f6
|
|||
|
3634264af3
|
|||
|
3515994a5a
|
|||
|
bdebe238ae
|
|||
|
ebf7d73d20
|
|||
|
e19a57efac
|
|||
|
034436f93e
|
|||
|
2710f7fc25
|
|||
|
d2276a53d0
|
|||
|
8701d7a40d
|
|||
|
94cfaf40e5
|
|||
|
c661a4b30b
|
|||
|
9b95e1d67a
|
|||
|
f7bf93fcfc
|
|||
|
08df049170
|
|||
|
65a536cdea
|
|||
|
d2fd84a6b5
|
|||
|
88429412b9
|
|||
|
ef309d603e
|
|||
|
fade2f978e
|
|||
|
a1760d518c
|
|||
|
9d33565081
|
|||
|
|
11fe12f095 | ||
|
aba390839b
|
|||
|
|
a6e7df55ff | ||
|
|
7e5e7b8f5f | ||
|
|
a34ba35889 | ||
|
|
616d15b6bd | ||
|
|
283444f0be | ||
|
|
e5e56c04a2 | ||
|
|
a34c176bcc | ||
|
e4d51cd51d
|
|||
|
a7b49de6ac
|
|||
|
fc86b31337
|
|||
|
d8f0679233
|
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@@ -7,8 +7,8 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
CI:
|
||||||
name: 🔨 Build
|
name: 🔨🚀 Build and deploy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 🚚 Checkout
|
- name: 🚚 Checkout
|
||||||
@@ -39,25 +39,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: build
|
path: build
|
||||||
deploy:
|
- name: Disable jekyll
|
||||||
name: 🚀 Deploy
|
run: touch build/.nojekyll
|
||||||
runs-on: ubuntu-latest
|
- name: Custom domain
|
||||||
needs: build
|
run: echo 'manager.charachorder.com' > build/CNAME
|
||||||
environment:
|
- run: git config user.name github-actions
|
||||||
name: Website
|
- run: git config user.email github-actions@github.com
|
||||||
url: https://dotio.theaninova.de
|
- run: git --work-tree build add --all
|
||||||
steps:
|
- run: git commit -m "Automatic Deploy action run by github-actions"
|
||||||
- name: 📦 Download build artifacts
|
- run: git push origin HEAD:gh-pages --force
|
||||||
uses: actions/download-artifact@v2.1.1
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: build
|
|
||||||
- name: 🚀 Deploy
|
|
||||||
uses: SamKirkland/web-deploy@v1
|
|
||||||
with:
|
|
||||||
target-server: ${{ secrets.SSH_SERVER }}
|
|
||||||
destination-path: ~/public_html/
|
|
||||||
source-path: ./build/
|
|
||||||
remote-user: ${{ secrets.SSH_USER }}
|
|
||||||
private-ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
ssh-port: ${{ secrets.SSH_PORT }}
|
|
||||||
|
|||||||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
name: "publish"
|
name: "publish desktop apps"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "desktop-app-v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules
|
|||||||
/package
|
/package
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
.direnv
|
||||||
!.env.example
|
!.env.example
|
||||||
venv
|
venv
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
|
|||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
...require("@theaninova/prettier-config"),
|
|
||||||
plugins: ["prettier-plugin-svelte"],
|
|
||||||
pluginSearchDirs: ["."],
|
|
||||||
overrides: [{files: "*.svelte", options: {parser: "svelte"}}],
|
|
||||||
}
|
|
||||||
15
README.md
15
README.md
@@ -1,15 +1,12 @@
|
|||||||
# amaCC1ng
|
# CharaChorder Device Manager
|
||||||
|
|
||||||

|
The official device manager and configuration tool for CharaChorder devices.
|
||||||

|
|
||||||
[](https://dotio.theaninova.de/)
|
|
||||||
|
|
||||||
_This project is not affiliated or endorsed with neither the original [dot i/o](https://www.iq-eq.io/) site, nor [CharaChorder](https://www.charachorder.com/)_
|

|
||||||
|

|
||||||
|
[](https://manager.charachorder.com/)
|
||||||
|
|
||||||
Get the latest desktop release [here](https://github.com/Theaninova/dotio/releases).
|
Get the latest desktop release [here](https://github.com/CharaChorder/DeviceManager/releases).
|
||||||
|
|
||||||
I aim to create a new site that offers an easier, visually pleasing
|
|
||||||
and more complete way to configure and learn CharaChorder devices.
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
|||||||
64
docs/BACKUP.md
Normal file
64
docs/BACKUP.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Chara Backup Format, Version 1
|
||||||
|
|
||||||
|
JSON Schema files: TBD
|
||||||
|
|
||||||
|
Chara backups are serialized using JSON, in this general format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The presence of the key `charaVersion` uniquely identifies the JSON file as a chara backup file and serves
|
||||||
|
as a discriminator against other generic JSON files. This key is mandatory for that reason.
|
||||||
|
|
||||||
|
## Type `layout`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "layout",
|
||||||
|
"device": "one",
|
||||||
|
"layers": [[], [], []]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Devices at the current point in time may be identified as either `lite` or `one`, more to come in the future.
|
||||||
|
|
||||||
|
Layers are serialized as an array of `[layer1, layer2, layer3]` in the internal order of the key, each specifying
|
||||||
|
an action code. Action codes of `0` are considered unassigned.
|
||||||
|
|
||||||
|
## Type `chords`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "chords",
|
||||||
|
"chords": [
|
||||||
|
[
|
||||||
|
[1, 2, 3],
|
||||||
|
[3, 4, 5]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[6, 7, 8],
|
||||||
|
[9, 10, 11]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Chords are serialized using a key-value mapping of chord action codes to actions.
|
||||||
|
|
||||||
|
## Type `settings`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "settings",
|
||||||
|
"settings": [0, 1, 3, 6]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Settings are serialized as an array of the values in the way they appear on the device.
|
||||||
71
flake.nix
71
flake.nix
@@ -4,31 +4,37 @@
|
|||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
|
outputs = {
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
self,
|
||||||
let
|
nixpkgs,
|
||||||
overlays = [ (import rust-overlay) ];
|
flake-utils,
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
rust-overlay,
|
||||||
rust-bin = pkgs.rust-bin.stable.latest.default.override {
|
}:
|
||||||
extensions = [ "rust-src" "rust-std" "clippy" "rust-analyzer" ];
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
};
|
overlays = [(import rust-overlay)];
|
||||||
fontMin = (pkgs.python311.withPackages(ps: with ps; [ brotli fonttools ] ++ (with fonttools.optional-dependencies; [ woff ])));
|
pkgs = import nixpkgs {inherit system overlays;};
|
||||||
tauriPkgs = nixpkgs.legacyPackages.${system};
|
rust-bin = pkgs.rust-bin.stable.latest.default.override {
|
||||||
libraries = with tauriPkgs; [
|
extensions = ["rust-src" "rust-std" "clippy" "rust-analyzer"];
|
||||||
webkitgtk
|
};
|
||||||
gtk3
|
fontMin = pkgs.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
|
||||||
cairo
|
tauriPkgs = nixpkgs.legacyPackages.${system};
|
||||||
gdk-pixbuf
|
libraries = with tauriPkgs; [
|
||||||
glib
|
webkitgtk
|
||||||
dbus
|
gtk3
|
||||||
openssl_3
|
cairo
|
||||||
librsvg
|
gdk-pixbuf
|
||||||
];
|
glib
|
||||||
packages = (with pkgs; [
|
dbus
|
||||||
|
openssl_3
|
||||||
|
librsvg
|
||||||
|
];
|
||||||
|
packages =
|
||||||
|
(with pkgs; [
|
||||||
nodejs_18
|
nodejs_18
|
||||||
rust-bin
|
rust-bin
|
||||||
fontMin
|
fontMin
|
||||||
]) ++ (with tauriPkgs; [
|
])
|
||||||
|
++ (with tauriPkgs; [
|
||||||
curl
|
curl
|
||||||
wget
|
wget
|
||||||
pkg-config
|
pkg-config
|
||||||
@@ -39,16 +45,15 @@
|
|||||||
libsoup
|
libsoup
|
||||||
webkitgtk
|
webkitgtk
|
||||||
librsvg
|
librsvg
|
||||||
# serial plugin
|
# serial plugin
|
||||||
udev
|
udev
|
||||||
]);
|
]);
|
||||||
in
|
in {
|
||||||
{
|
devShell = pkgs.mkShell {
|
||||||
devShell = pkgs.mkShell {
|
buildInputs = packages;
|
||||||
buildInputs = packages;
|
shellHook = ''
|
||||||
shellHook = ''
|
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
||||||
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
'';
|
||||||
'';
|
};
|
||||||
};
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
export interface IconsConfig {
|
/** @type {import('./src/tools/icons-config').IconsConfig} */
|
||||||
codePoints: Record<string, string>
|
const config = {
|
||||||
inputPath: string
|
|
||||||
outputPath: string
|
|
||||||
icons: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: IconsConfig = {
|
|
||||||
inputPath:
|
inputPath:
|
||||||
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
||||||
outputPath: "src/lib/assets/icons.min.woff2",
|
outputPath: "src/lib/assets/icons.min.woff2",
|
||||||
icons: [
|
icons: [
|
||||||
|
"adjust",
|
||||||
|
"add",
|
||||||
"piano",
|
"piano",
|
||||||
"keyboard",
|
"keyboard",
|
||||||
"settings",
|
"settings",
|
||||||
@@ -25,6 +21,7 @@ const config: IconsConfig = {
|
|||||||
"cable",
|
"cable",
|
||||||
"person",
|
"person",
|
||||||
"sync",
|
"sync",
|
||||||
|
"school",
|
||||||
"restart_alt",
|
"restart_alt",
|
||||||
"usb",
|
"usb",
|
||||||
"usb_off",
|
"usb_off",
|
||||||
@@ -44,6 +41,7 @@ const config: IconsConfig = {
|
|||||||
"save",
|
"save",
|
||||||
"settings_backup_restore",
|
"settings_backup_restore",
|
||||||
"sort",
|
"sort",
|
||||||
|
"shopping_bag",
|
||||||
"filter_list",
|
"filter_list",
|
||||||
"settings_power",
|
"settings_power",
|
||||||
"link",
|
"link",
|
||||||
@@ -67,6 +65,35 @@ const config: IconsConfig = {
|
|||||||
"bolt",
|
"bolt",
|
||||||
"undo",
|
"undo",
|
||||||
"redo",
|
"redo",
|
||||||
|
"navigate_before",
|
||||||
|
"navigate_next",
|
||||||
|
"print",
|
||||||
|
"restore_from_trash",
|
||||||
|
"history",
|
||||||
|
"history_toggle_off",
|
||||||
|
"sentiment_satisfied",
|
||||||
|
"sentiment_dissatisfied",
|
||||||
|
"sentiment_very_satisfied",
|
||||||
|
"sentiment_neutral",
|
||||||
|
"sentiment_very_dissatisfied",
|
||||||
|
"sentiment_excited",
|
||||||
|
"sentiment_frustrated",
|
||||||
|
"sentiment_calm",
|
||||||
|
"sentiment_stressed",
|
||||||
|
"sentiment_extremely_dissatisfied",
|
||||||
|
"sentiment_sad",
|
||||||
|
"sentiment_content",
|
||||||
|
"sentiment_worried",
|
||||||
|
"timer",
|
||||||
|
"target",
|
||||||
|
"download",
|
||||||
|
"download_2",
|
||||||
|
"upload_2",
|
||||||
|
"stat_minus_2",
|
||||||
|
"stat_2",
|
||||||
|
"description",
|
||||||
|
"add_circle",
|
||||||
|
"refresh",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
@@ -80,7 +107,12 @@ const config: IconsConfig = {
|
|||||||
light_mode: "e518",
|
light_mode: "e518",
|
||||||
upload_file: "e9fc",
|
upload_file: "e9fc",
|
||||||
no_sound: "e710",
|
no_sound: "e710",
|
||||||
|
sentiment_extremely_dissatisfied: "f194",
|
||||||
|
download_2: "f523",
|
||||||
|
upload_2: "ff52",
|
||||||
|
stat_minus_2: "e69c",
|
||||||
|
stat_2: "e699",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default config
|
export default config;
|
||||||
5699
package-lock.json
generated
5699
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
@@ -1,83 +1,81 @@
|
|||||||
{
|
{
|
||||||
"name": "amacc1ng",
|
"name": "charachorder-device-manager",
|
||||||
"version": "0.6.5",
|
"version": "1.5.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Theaninova/amacc1ng.git"
|
"url": "https://github.com/CharaChorder/DeviceManager.git"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Theaninova/amacc1ng",
|
"homepage": "https://docs.charachorder.com",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Theaninova/amacc1ng/issues"
|
"url": "https://github.com/CharaChorder/DeviceManager/issues"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm-run-all --parallel vite typesafe-i18n",
|
"dev": "npm-run-all --parallel vite typesafe-i18n",
|
||||||
|
"dev:external": "npm-run-all --parallel vite:external typesafe-i18n",
|
||||||
"dev:tauri": "tauri dev",
|
"dev:tauri": "tauri dev",
|
||||||
"vite": "vite dev",
|
"vite": "vite dev",
|
||||||
|
"vite:external": "vite --host",
|
||||||
"build": "typesafe-i18n --no-watch && vite build",
|
"build": "typesafe-i18n --no-watch && vite build",
|
||||||
"build:tauri": "tauri build",
|
"build:tauri": "tauri build",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"test": "vitest run --coverage",
|
"test": "vitest run --coverage",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"postinstall": "patch-package",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
"minify-icons": "node src/tools/minify-icon-font.js",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
"version": "node src/tools/version.js && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
|
||||||
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
|
"lint": "prettier --check .",
|
||||||
"version": "ts-node-esm src/tools/version.ts && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --plugin-search-dir . --check .",
|
|
||||||
"format": "prettier --plugin-search-dir . --write .",
|
|
||||||
"typesafe-i18n": "typesafe-i18n"
|
"typesafe-i18n": "typesafe-i18n"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.15.0",
|
||||||
"@codemirror/commands": "^6.2.5",
|
"@codemirror/commands": "^6.3.3",
|
||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/language": "^6.9.0",
|
"@codemirror/language": "^6.10.1",
|
||||||
"@codemirror/state": "^6.2.1",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.11",
|
"@fontsource-variable/material-symbols-rounded": "^5.0.27",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.12",
|
"@fontsource-variable/noto-sans-mono": "^5.0.19",
|
||||||
"@material/material-color-utilities": "^0.2.7",
|
"@material/material-color-utilities": "^0.2.7",
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
"@sveltejs/adapter-static": "^2.0.3",
|
"@sveltejs/adapter-static": "^2.0.3",
|
||||||
"@sveltejs/kit": "^1.24.1",
|
"@sveltejs/kit": "^1.30.4",
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.5",
|
"@sveltejs/vite-plugin-svelte": "^2.5.3",
|
||||||
"@tauri-apps/api": "^1.4.0",
|
"@tauri-apps/api": "^1.5.3",
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@tauri-apps/cli": "^1.5.11",
|
||||||
"@theaninova/prettier-config": "^1.0.0",
|
"@types/dom-view-transitions": "^1.0.4",
|
||||||
"@types/dom-view-transitions": "^1.0.1",
|
"@types/flexsearch": "^0.7.6",
|
||||||
"@types/flexsearch": "^0.7.3",
|
"@types/w3c-web-serial": "^1.0.6",
|
||||||
"@types/w3c-web-serial": "^1.0.3",
|
"@types/w3c-web-usb": "^1.0.10",
|
||||||
"@vite-pwa/sveltekit": "^0.2.7",
|
"@vite-pwa/sveltekit": "^0.2.10",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.19",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"cypress": "^13.1.0",
|
"cypress": "^13.7.2",
|
||||||
"flexsearch": "^0.7.31",
|
"flexsearch": "^0.7.43",
|
||||||
"fontkit": "^2.0.2",
|
"fontkit": "^2.0.2",
|
||||||
"glob": "^10.3.4",
|
"glob": "^10.3.12",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"patch-package": "^8.0.0",
|
"prettier": "^3.2.5",
|
||||||
"prettier": "^3.0.3",
|
"prettier-plugin-svelte": "^3.2.2",
|
||||||
"prettier-plugin-svelte": "^3.0.3",
|
"sass": "^1.74.1",
|
||||||
"sass": "^1.66.1",
|
"stylelint": "^15.11.0",
|
||||||
"stylelint": "^15.10.3",
|
"stylelint-config-clean-order": "^5.4.2",
|
||||||
"stylelint-config-clean-order": "^5.2.0",
|
|
||||||
"stylelint-config-html": "^1.1.0",
|
"stylelint-config-html": "^1.1.0",
|
||||||
"stylelint-config-prettier-scss": "^1.0.0",
|
"stylelint-config-prettier-scss": "^1.0.0",
|
||||||
"stylelint-config-recommended-scss": "^13.0.0",
|
"stylelint-config-recommended-scss": "^13.1.0",
|
||||||
"stylelint-config-standard-scss": "^11.0.0",
|
"stylelint-config-standard-scss": "^11.1.0",
|
||||||
"svelte": "^4.2.0",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.5.1",
|
"svelte-check": "^3.6.9",
|
||||||
"svelte-preprocess": "^5.0.4",
|
"svelte-preprocess": "^5.1.3",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typesafe-i18n": "^5.26.2",
|
"typesafe-i18n": "^5.26.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.4.4",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.5.3",
|
||||||
"vite-plugin-mkcert": "^1.16.0",
|
"vite-plugin-mkcert": "^1.17.5",
|
||||||
"vite-plugin-pwa": "^0.16.5",
|
"vite-plugin-pwa": "^0.17.5",
|
||||||
"vitest": "^0.34.4"
|
"vitest": "^0.34.6"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
diff --git a/node_modules/@types/flexsearch/index.d.ts b/node_modules/@types/flexsearch/index.d.ts
|
|
||||||
index ecde8e7..64a5f1e 100755
|
|
||||||
--- a/node_modules/@types/flexsearch/index.d.ts
|
|
||||||
+++ b/node_modules/@types/flexsearch/index.d.ts
|
|
||||||
@@ -6,7 +6,6 @@
|
|
||||||
/************************************/
|
|
||||||
/* Utils */
|
|
||||||
/************************************/
|
|
||||||
-export type Id = number | string;
|
|
||||||
export type Limit = number;
|
|
||||||
export type ExportHandler<T> = (id: string | number, value: T) => void;
|
|
||||||
export type AsyncCallback<T = undefined> = T extends undefined ? () => void : (result: T) => void;
|
|
||||||
@@ -165,7 +164,7 @@ export type IndexSearchResult = Id[];
|
|
||||||
* * Usage: https://github.com/nextapps-de/flexsearch#usage
|
|
||||||
*/
|
|
||||||
|
|
||||||
-export class Index {
|
|
||||||
+export default class Index<ID extends number | string = number> {
|
|
||||||
constructor(x?: Preset | IndexOptions<string>);
|
|
||||||
add(id: Id, item: string): this;
|
|
||||||
append(id: Id, item: string): this;
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
diff --git a/node_modules/flexsearch/index.d.ts b/node_modules/flexsearch/index.d.ts
|
|
||||||
deleted file mode 100644
|
|
||||||
index 9f39f41..0000000
|
|
||||||
--- a/node_modules/flexsearch/index.d.ts
|
|
||||||
+++ /dev/null
|
|
||||||
@@ -1,147 +0,0 @@
|
|
||||||
-declare module "flexsearch" {
|
|
||||||
- export interface Index<T> {
|
|
||||||
- readonly id: string;
|
|
||||||
- readonly index: string;
|
|
||||||
- readonly length: number;
|
|
||||||
-
|
|
||||||
- init(options?: CreateOptions): this;
|
|
||||||
- info(): {
|
|
||||||
- id: any;
|
|
||||||
- items: any;
|
|
||||||
- cache: any;
|
|
||||||
- matcher: number;
|
|
||||||
- worker: any;
|
|
||||||
- threshold: any;
|
|
||||||
- depth: any;
|
|
||||||
- resolution: any;
|
|
||||||
- contextual: boolean;
|
|
||||||
- };
|
|
||||||
- add(o: T): this;
|
|
||||||
- add(id: number, o: string): this;
|
|
||||||
-
|
|
||||||
- // Result without pagination -> T[]
|
|
||||||
- search(
|
|
||||||
- query: string,
|
|
||||||
- options: number | SearchOptions,
|
|
||||||
- callback: (results: T[]) => void
|
|
||||||
- ): void;
|
|
||||||
- search(query: string, options?: number | SearchOptions): Promise<T[]>;
|
|
||||||
- search(
|
|
||||||
- options: SearchOptions & { query: string },
|
|
||||||
- callback: (results: T[]) => void
|
|
||||||
- ): void;
|
|
||||||
- search(options: SearchOptions & { query: string }): Promise<T[]>;
|
|
||||||
-
|
|
||||||
- // Result with pagination -> SearchResults<T>
|
|
||||||
- search(
|
|
||||||
- query: string,
|
|
||||||
- options: number | (SearchOptions & { page?: boolean | Cursor }),
|
|
||||||
- callback: (results: SearchResults<T>) => void
|
|
||||||
- ): void;
|
|
||||||
- search(
|
|
||||||
- query: string,
|
|
||||||
- options?: number | (SearchOptions & { page?: boolean | Cursor })
|
|
||||||
- ): Promise<SearchResults<T>>;
|
|
||||||
- search(
|
|
||||||
- options: SearchOptions & { query: string; page?: boolean | Cursor },
|
|
||||||
- callback: (results: SearchResults<T>) => void
|
|
||||||
- ): void;
|
|
||||||
- search(
|
|
||||||
- options: SearchOptions & { query: string; page?: boolean | Cursor }
|
|
||||||
- ): Promise<SearchResults<T>>;
|
|
||||||
-
|
|
||||||
- update(id: number, o: T): this;
|
|
||||||
- remove(id: number): this;
|
|
||||||
- clear(): this;
|
|
||||||
- destroy(): this;
|
|
||||||
- addMatcher(matcher: Matcher): this;
|
|
||||||
-
|
|
||||||
- where(whereObj: { [key: string]: string } | ((o: T) => boolean)): T[];
|
|
||||||
- encode(str: string): string;
|
|
||||||
- export(
|
|
||||||
- callback: (key: string, data: any) => any,
|
|
||||||
- self?: this,
|
|
||||||
- field?: string,
|
|
||||||
- index_doc?: Number,
|
|
||||||
- index?: Number
|
|
||||||
- ): Promise<boolean>;
|
|
||||||
- import(exported: string): this;
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- interface SearchOptions {
|
|
||||||
- limit?: number;
|
|
||||||
- suggest?: boolean;
|
|
||||||
- where?: { [key: string]: string };
|
|
||||||
- field?: string | string[];
|
|
||||||
- bool?: "and" | "or" | "not";
|
|
||||||
- //TODO: Sorting
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- interface SearchResults<T> {
|
|
||||||
- page?: Cursor;
|
|
||||||
- next?: Cursor;
|
|
||||||
- result: T[];
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- interface Document {
|
|
||||||
- id: string;
|
|
||||||
- field: any;
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- export type CreateOptions = {
|
|
||||||
- profile?: IndexProfile;
|
|
||||||
- tokenize?: DefaultTokenizer | TokenizerFn;
|
|
||||||
- split?: RegExp;
|
|
||||||
- encode?: DefaultEncoder | EncoderFn | false;
|
|
||||||
- cache?: boolean | number;
|
|
||||||
- async?: boolean;
|
|
||||||
- worker?: false | number;
|
|
||||||
- depth?: false | number;
|
|
||||||
- threshold?: false | number;
|
|
||||||
- resolution?: number;
|
|
||||||
- stemmer?: Stemmer | string | false;
|
|
||||||
- filter?: FilterFn | string | false;
|
|
||||||
- rtl?: boolean;
|
|
||||||
- doc?: Document;
|
|
||||||
- };
|
|
||||||
-
|
|
||||||
- // limit number Sets the limit of results.
|
|
||||||
- // suggest true, false Enables suggestions in results.
|
|
||||||
- // where object Use a where-clause for non-indexed fields.
|
|
||||||
- // field string, Array<string> Sets the document fields which should be searched. When no field is set, all fields will be searched. Custom options per field are also supported.
|
|
||||||
- // bool "and", "or" Sets the used logical operator when searching through multiple fields.
|
|
||||||
- // page true, false, cursor Enables paginated results.
|
|
||||||
-
|
|
||||||
- type IndexProfile =
|
|
||||||
- | "memory"
|
|
||||||
- | "speed"
|
|
||||||
- | "match"
|
|
||||||
- | "score"
|
|
||||||
- | "balance"
|
|
||||||
- | "fast";
|
|
||||||
- type DefaultTokenizer = "strict" | "forward" | "reverse" | "full";
|
|
||||||
- type TokenizerFn = (str: string) => string[];
|
|
||||||
- type DefaultEncoder = "icase" | "simple" | "advanced" | "extra" | "balance";
|
|
||||||
- type EncoderFn = (str: string) => string;
|
|
||||||
- type Stemmer = { [key: string]: string };
|
|
||||||
- type Matcher = { [key: string]: string };
|
|
||||||
- type FilterFn = (str: string) => boolean;
|
|
||||||
- type Cursor = string;
|
|
||||||
-
|
|
||||||
- export default class FlexSearch {
|
|
||||||
- static create<T>(options?: CreateOptions): Index<T>;
|
|
||||||
- static registerMatcher(matcher: Matcher): typeof FlexSearch;
|
|
||||||
- static registerEncoder(name: string, encoder: EncoderFn): typeof FlexSearch;
|
|
||||||
- static registerLanguage(
|
|
||||||
- lang: string,
|
|
||||||
- options: { stemmer?: Stemmer; filter?: string[] }
|
|
||||||
- ): typeof FlexSearch;
|
|
||||||
- static encode(name: string, str: string): string;
|
|
||||||
- }
|
|
||||||
-}
|
|
||||||
-
|
|
||||||
-// FlexSearch.create(<options>)
|
|
||||||
-// FlexSearch.registerMatcher({KEY: VALUE})
|
|
||||||
-// FlexSearch.registerEncoder(name, encoder)
|
|
||||||
-// FlexSearch.registerLanguage(lang, {stemmer:{}, filter:[]})
|
|
||||||
-// FlexSearch.encode(name, string)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.6.5"
|
version = "1.5.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
||||||
license = "AGPL-3"
|
license = "AGPL-3"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"devPath": "http://localhost:5173",
|
"devPath": "http://localhost:5173",
|
||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": { "productName": "amacc1ng", "version": "0.6.5" },
|
"package": { "productName": "amacc1ng", "version": "1.5.0" },
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": { "all": false },
|
"allowlist": { "all": false },
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
@@ -11,4 +11,4 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export {};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
24
src/env.d.ts
vendored
24
src/env.d.ts
vendored
@@ -1,17 +1,21 @@
|
|||||||
/// <references types="vite/client" />
|
/// <references types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly TAURI_FAMILY?: string
|
readonly TAURI_FAMILY?: string;
|
||||||
readonly TAURI_PLATFORM_VERSION?: string
|
readonly TAURI_PLATFORM_VERSION?: string;
|
||||||
readonly TAURI_TARGET_TRIPLE?: string
|
readonly TAURI_TARGET_TRIPLE?: string;
|
||||||
readonly TAURI_ARCH?: string
|
readonly TAURI_ARCH?: string;
|
||||||
readonly TAURI_DEBUG?: boolean
|
readonly TAURI_DEBUG?: boolean;
|
||||||
readonly TAURI_PLATFORM_TYPE?: string
|
readonly TAURI_PLATFORM_TYPE?: string;
|
||||||
|
|
||||||
|
readonly VITE_HOMEPAGE_URL: string;
|
||||||
|
readonly VITE_BUGS_URL: string;
|
||||||
|
readonly VITE_DOCS_URL: string;
|
||||||
|
readonly VITE_LEARN_URL: string;
|
||||||
|
readonly VITE_LATEST_FIRMWARE: string;
|
||||||
|
readonly VITE_STORE_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const HOMEPAGE_URL: string
|
|
||||||
declare const BUGS_URL: string
|
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import type {Translation} from "../i18n-types"
|
import type { Translation } from "../i18n-types";
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
TITLE: "amaCC1ng",
|
TITLE: "CharaChorder Gerätemanager",
|
||||||
|
DESCRIPTION: "Gerätemanager und Konfigurationstool für CharaChorder Geräte.",
|
||||||
saveActions: {
|
saveActions: {
|
||||||
UNDO: "Rückgängig",
|
UNDO: "Rückgängig (<kbd class='icon'>shift</kbd> halten um alle Änderungen rückgängig zu machen)",
|
||||||
REDO: "Wiederholen",
|
REDO: "Wiederholen",
|
||||||
APPLY: "Anwenden",
|
SAVE: "Speichern",
|
||||||
SAVE: "Änderungen auf das Gerät schreiben",
|
},
|
||||||
|
update: {
|
||||||
|
TITLE: "Gerät aktualisieren",
|
||||||
|
},
|
||||||
|
sync: {
|
||||||
|
TITLE_READ: "Neueste Änderungen werden abgerufen",
|
||||||
|
TITLE_WRITE: "Änderungen werden gespeichert",
|
||||||
|
RELOAD: "Neu laden",
|
||||||
},
|
},
|
||||||
backup: {
|
backup: {
|
||||||
TITLE: "Sicherungskopie",
|
TITLE: "Lokale Kopie",
|
||||||
|
INDIVIDUAL: "Einzeldateien",
|
||||||
DISCLAIMER:
|
DISCLAIMER:
|
||||||
"Sicherungskopien verlassen unter keinen Umständen diesen Computer und werden nie mit uns geteilt oder auf Server hochgeladen.",
|
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
|
||||||
DOWNLOAD: "Kopie Speichern",
|
DOWNLOAD: "Alles herunterladen",
|
||||||
RESTORE: "Wiederherstellen",
|
RESTORE: "Wiederherstellen",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
@@ -21,12 +30,24 @@ const de = {
|
|||||||
actionSearch: {
|
actionSearch: {
|
||||||
PLACEHOLDER: "Nach Aktionen suchen",
|
PLACEHOLDER: "Nach Aktionen suchen",
|
||||||
CURRENT_ACTION: "Aktuelle Aktion",
|
CURRENT_ACTION: "Aktuelle Aktion",
|
||||||
|
NEXT_ACTION: "Aktion nach dem nächsten Speichern",
|
||||||
DELETE: "Entfernen",
|
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: {
|
share: {
|
||||||
|
TITLE: "Teilen",
|
||||||
URL_COPIED: "Teilbare URL kopiert!",
|
URL_COPIED: "Teilbare URL kopiert!",
|
||||||
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
||||||
},
|
},
|
||||||
|
print: {
|
||||||
|
TITLE: "Drucken",
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
TITLE: "Profil",
|
TITLE: "Profil",
|
||||||
LANGUAGE: "Sprache",
|
LANGUAGE: "Sprache",
|
||||||
@@ -44,10 +65,15 @@ const de = {
|
|||||||
DISCONNECT: "Entfernen",
|
DISCONNECT: "Entfernen",
|
||||||
TERMINAL: "Konsole",
|
TERMINAL: "Konsole",
|
||||||
APPLY_SETTINGS: "Änderungen auf das Gerät brennen",
|
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. Flatpak und Snap Anwendungen benötigen zusätzliche Berechtigungen oder funktionieren möglicherweise gar nicht.",
|
||||||
bootMenu: {
|
bootMenu: {
|
||||||
TITLE: "Bootmenü",
|
TITLE: "Bootmenü",
|
||||||
REBOOT: "Neustarten",
|
REBOOT: "Neustarten",
|
||||||
BOOTLOADER: "Bootloader",
|
BOOTLOADER: "Bootloader",
|
||||||
|
POWER_WARNING:
|
||||||
|
"Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
browserWarning: {
|
browserWarning: {
|
||||||
@@ -59,15 +85,47 @@ const de = {
|
|||||||
INFO_BROWSER_PREFIX:
|
INFO_BROWSER_PREFIX:
|
||||||
"Auch wenn alle Chromium-basieren Desktop Browser diese Voraussetzung grundsätzlich erfüllen, haben einige Browser ",
|
"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_INFIX: "wie zum Beispiel Brave",
|
||||||
INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.",
|
INFO_BROWSER_SUFFIX:
|
||||||
DOWNLOAD_APP: "Desktop-app herunterladen",
|
" sich bewusst dazu entschieden die API zu deaktivieren.",
|
||||||
|
DOWNLOAD_APP:
|
||||||
|
"Chrome oder Edge werden offiziell unterstützt, andere Browser könnten aber auch funktionieren.",
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
TITLE: "Änderungen importieren",
|
||||||
|
ALL_CHANGES: "Alle Änderungen",
|
||||||
|
layout: {
|
||||||
|
TITLE: "{0} veränderte Belegung{{:|en}}",
|
||||||
|
LAYER: "{changes} Belegung{{changes:|en}} in Ebene {layer} ändern",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
TITLE: "{0} Einstellung{{|en}} anpassen",
|
||||||
|
},
|
||||||
|
chords: {
|
||||||
|
TITLE: "{0} Akkorde",
|
||||||
|
NEW_CHORDS: "{0} neue Akkord{{|e}} hinzufügen",
|
||||||
|
CHANGED_CHORDS: "{0} Akkord{{|e}} ersetzen",
|
||||||
|
DELETED_CHORDS: "{0} Akkord{{|e}} zum löschen markieren",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
configure: {
|
configure: {
|
||||||
chords: {
|
chords: {
|
||||||
TITLE: "Akkorde",
|
TITLE: "Akkorde",
|
||||||
|
HOLD_KEYS: "Akkord halten",
|
||||||
|
NEW_CHORD: "Neuer Akkord",
|
||||||
|
DUPLICATE: "Akkord existiert bereits",
|
||||||
search: {
|
search: {
|
||||||
PLACEHOLDER: "{0} Akkord{{|e}} durchsuchen",
|
PLACEHOLDER: "{0} Akkord{{|e}} durchsuchen",
|
||||||
|
NO_RESULTS: "Keine Ergebnisse",
|
||||||
},
|
},
|
||||||
|
conflict: {
|
||||||
|
TITLE: "Akkordkonflikt",
|
||||||
|
DESCRIPTION:
|
||||||
|
"Der Akkord würde einen bereits existierenden Akkord überschreiben. Wirklich fortfahren?",
|
||||||
|
CONFIRM: "Überschreiben",
|
||||||
|
ABORT: "Überspringen",
|
||||||
|
},
|
||||||
|
VOCABULARY: "Vokabelliste",
|
||||||
|
TRY_TYPING: "Versuche hier zu tippen",
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
TITLE: "Layout",
|
TITLE: "Layout",
|
||||||
@@ -81,6 +139,6 @@ const de = {
|
|||||||
RUN: "Ausführen",
|
RUN: "Ausführen",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies Translation
|
} satisfies Translation;
|
||||||
|
|
||||||
export default de
|
export default de;
|
||||||
|
|||||||
@@ -1,31 +1,53 @@
|
|||||||
import type {BaseTranslation} from "../i18n-types"
|
import type { BaseTranslation } from "../i18n-types";
|
||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
TITLE: "amaCC1ng",
|
TITLE: "CharaChorder Device Manager",
|
||||||
|
DESCRIPTION:
|
||||||
|
"The device manager and configuration tool for CharaChorder devices.",
|
||||||
saveActions: {
|
saveActions: {
|
||||||
UNDO: "Undo",
|
UNDO: "Undo (hold <kbd class='icon'>shift</kbd> to undo all changes)",
|
||||||
REDO: "Redo",
|
REDO: "Redo",
|
||||||
APPLY: "Apply",
|
SAVE: "Save",
|
||||||
SAVE: "Write changes to your device",
|
},
|
||||||
|
update: {
|
||||||
|
TITLE: "Update your device",
|
||||||
},
|
},
|
||||||
backup: {
|
backup: {
|
||||||
TITLE: "Local Backup",
|
TITLE: "Local backup",
|
||||||
DISCLAIMER: "Backups remain on your computer and are never shared with us or uploaded to our servers.",
|
INDIVIDUAL: "Individual backups",
|
||||||
DOWNLOAD: "Download Backup",
|
DISCLAIMER:
|
||||||
|
"A backup is made and stored in this browser, and always remains only on your computer.",
|
||||||
|
DOWNLOAD: "Download Everything",
|
||||||
RESTORE: "Restore",
|
RESTORE: "Restore",
|
||||||
},
|
},
|
||||||
|
sync: {
|
||||||
|
TITLE_READ: "Reading latest changes",
|
||||||
|
TITLE_WRITE: "Saving changes to device",
|
||||||
|
RELOAD: "Reload",
|
||||||
|
},
|
||||||
modal: {
|
modal: {
|
||||||
CLOSE: "Close",
|
CLOSE: "Close",
|
||||||
},
|
},
|
||||||
actionSearch: {
|
actionSearch: {
|
||||||
PLACEHOLDER: "Search for actions",
|
PLACEHOLDER: "Search for actions",
|
||||||
CURRENT_ACTION: "Current action",
|
CURRENT_ACTION: "Current action",
|
||||||
|
NEXT_ACTION: "Action after next save",
|
||||||
DELETE: "Remove",
|
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: {
|
share: {
|
||||||
|
TITLE: "Share",
|
||||||
URL_COPIED: "Sharable URL copied!",
|
URL_COPIED: "Sharable URL copied!",
|
||||||
EXTRA_DOWNLOAD: "Download as file",
|
EXTRA_DOWNLOAD: "Download as file",
|
||||||
},
|
},
|
||||||
|
print: {
|
||||||
|
TITLE: "Print",
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
TITLE: "Profile",
|
TITLE: "Profile",
|
||||||
LANGUAGE: "Language",
|
LANGUAGE: "Language",
|
||||||
@@ -43,29 +65,66 @@ const en = {
|
|||||||
DISCONNECT: "Disconnect",
|
DISCONNECT: "Disconnect",
|
||||||
TERMINAL: "Terminal",
|
TERMINAL: "Terminal",
|
||||||
APPLY_SETTINGS: "Flash changes to device",
|
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. Flatpak or Snap versions in particular might need additional permissions or may not work at all.",
|
||||||
bootMenu: {
|
bootMenu: {
|
||||||
TITLE: "Boot Menu",
|
TITLE: "Boot Menu",
|
||||||
REBOOT: "Reboot",
|
REBOOT: "Reboot",
|
||||||
BOOTLOADER: "Bootloader",
|
BOOTLOADER: "Bootloader",
|
||||||
|
POWER_WARNING:
|
||||||
|
"To reboot from bootloader you need to physically reconnect your device.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
browserWarning: {
|
browserWarning: {
|
||||||
TITLE: "Warning",
|
TITLE: "Warning",
|
||||||
INFO_SERIAL_PREFIX: "Your current browser is not supported due to this site's unique requirement for ",
|
INFO_SERIAL_PREFIX:
|
||||||
|
"Your current browser is not supported due to this site's unique requirement for ",
|
||||||
INFO_SERIAL_INFIX: "serial connections",
|
INFO_SERIAL_INFIX: "serial connections",
|
||||||
INFO_SERIAL_SUFFIX: ".",
|
INFO_SERIAL_SUFFIX: ".",
|
||||||
INFO_BROWSER_PREFIX:
|
INFO_BROWSER_PREFIX:
|
||||||
"Though all chromium-based desktop browsers fulfill this requirement, some derivations such as Brave ",
|
"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_INFIX: "have been known to disable the API intentionally",
|
||||||
INFO_BROWSER_SUFFIX: ".",
|
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",
|
||||||
|
ALL_CHANGES: "All changes",
|
||||||
|
layout: {
|
||||||
|
TITLE: "{0} layout change{{|s}}",
|
||||||
|
LAYER: "Update {changes} key{{changes:|s}} in layer {layer}",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
TITLE: "Update {0} setting{{|s}}",
|
||||||
|
},
|
||||||
|
chords: {
|
||||||
|
TITLE: "{0} chords",
|
||||||
|
NEW_CHORDS: "Add {0} new chord{{|s}}",
|
||||||
|
CHANGED_CHORDS: "Replace {0} chord{{|s}}",
|
||||||
|
DELETED_CHORDS: "Mark {0} chord{{|s}} for deletion",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
configure: {
|
configure: {
|
||||||
chords: {
|
chords: {
|
||||||
TITLE: "Chords",
|
TITLE: "Chords",
|
||||||
|
HOLD_KEYS: "Hold chord",
|
||||||
|
NEW_CHORD: "New chord",
|
||||||
|
DUPLICATE: "Chord already exists",
|
||||||
search: {
|
search: {
|
||||||
PLACEHOLDER: "Search {0} chord{{|s}}",
|
PLACEHOLDER: "Search {0} chord{{|s}}",
|
||||||
|
NO_RESULTS: "No results",
|
||||||
},
|
},
|
||||||
|
conflict: {
|
||||||
|
TITLE: "Chord conflict",
|
||||||
|
DESCRIPTION:
|
||||||
|
"Your chord conflicts with an existing chord. Are you sure you want to overwrite this chord?",
|
||||||
|
CONFIRM: "Overwrite",
|
||||||
|
ABORT: "Skip",
|
||||||
|
},
|
||||||
|
VOCABULARY: "Vocabulary",
|
||||||
|
TRY_TYPING: "Try typing here",
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
TITLE: "Layout",
|
TITLE: "Layout",
|
||||||
@@ -79,6 +138,6 @@ const en = {
|
|||||||
RUN: "Run",
|
RUN: "Run",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies BaseTranslation
|
} satisfies BaseTranslation;
|
||||||
|
|
||||||
export default en
|
export default en;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type {FormattersInitializer} from "typesafe-i18n"
|
import type { FormattersInitializer } from "typesafe-i18n";
|
||||||
import type {Locales, Formatters} from "./i18n-types"
|
import type { Locales, Formatters } from "./i18n-types";
|
||||||
|
|
||||||
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
export const initFormatters: FormattersInitializer<Locales, Formatters> = (
|
||||||
|
_locale: Locales,
|
||||||
|
) => {
|
||||||
const formatters: Formatters = {
|
const formatters: Formatters = {
|
||||||
// add your formatter functions here
|
// add your formatter functions here
|
||||||
}
|
};
|
||||||
|
|
||||||
return formatters
|
return formatters;
|
||||||
}
|
};
|
||||||
|
|||||||
144
src/lib/assets/keymaps/ascii-macros.yml
Normal file
144
src/lib/assets/keymaps/ascii-macros.yml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
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
|
||||||
|
43:
|
||||||
|
id: "+"
|
||||||
|
title: Plus
|
||||||
|
58:
|
||||||
|
id: ":"
|
||||||
|
title: Colon
|
||||||
|
60:
|
||||||
|
id: "<"
|
||||||
|
title: Less Than
|
||||||
|
62:
|
||||||
|
id: ">"
|
||||||
|
title: Greater Than
|
||||||
|
63:
|
||||||
|
id: "?"
|
||||||
|
title: Question Mark
|
||||||
|
64:
|
||||||
|
id: "@"
|
||||||
|
title: At Symbol
|
||||||
|
65:
|
||||||
|
id: "A"
|
||||||
|
title: Uppercase A
|
||||||
|
66:
|
||||||
|
id: "B"
|
||||||
|
title: Uppercase B
|
||||||
|
67:
|
||||||
|
id: "C"
|
||||||
|
title: Uppercase C
|
||||||
|
68:
|
||||||
|
id: "D"
|
||||||
|
title: Uppercase D
|
||||||
|
69:
|
||||||
|
id: "E"
|
||||||
|
title: Uppercase E
|
||||||
|
70:
|
||||||
|
id: "F"
|
||||||
|
title: Uppercase F
|
||||||
|
71:
|
||||||
|
id: "G"
|
||||||
|
title: Uppercase G
|
||||||
|
72:
|
||||||
|
id: "H"
|
||||||
|
title: Uppercase H
|
||||||
|
73:
|
||||||
|
id: "I"
|
||||||
|
title: Uppercase I
|
||||||
|
74:
|
||||||
|
id: "J"
|
||||||
|
title: Uppercase J
|
||||||
|
75:
|
||||||
|
id: "K"
|
||||||
|
title: Uppercase K
|
||||||
|
76:
|
||||||
|
id: "L"
|
||||||
|
title: Uppercase L
|
||||||
|
77:
|
||||||
|
id: "M"
|
||||||
|
title: Uppercase M
|
||||||
|
78:
|
||||||
|
id: "N"
|
||||||
|
title: Uppercase N
|
||||||
|
79:
|
||||||
|
id: "O"
|
||||||
|
title: Uppercase O
|
||||||
|
80:
|
||||||
|
id: "P"
|
||||||
|
title: Uppercase P
|
||||||
|
81:
|
||||||
|
id: "Q"
|
||||||
|
title: Uppercase Q
|
||||||
|
82:
|
||||||
|
id: "R"
|
||||||
|
title: Uppercase R
|
||||||
|
83:
|
||||||
|
id: "S"
|
||||||
|
title: Uppercase S
|
||||||
|
84:
|
||||||
|
id: "T"
|
||||||
|
title: Uppercase T
|
||||||
|
85:
|
||||||
|
id: "U"
|
||||||
|
title: Uppercase U
|
||||||
|
86:
|
||||||
|
id: "V"
|
||||||
|
title: Uppercase V
|
||||||
|
87:
|
||||||
|
id: "W"
|
||||||
|
title: Uppercase W
|
||||||
|
88:
|
||||||
|
id: "X"
|
||||||
|
title: Uppercase X
|
||||||
|
89:
|
||||||
|
id: "Y"
|
||||||
|
title: Uppercase Y
|
||||||
|
90:
|
||||||
|
id: "Z"
|
||||||
|
title: Uppercase Z
|
||||||
|
94:
|
||||||
|
id: "^"
|
||||||
|
title: Caret
|
||||||
|
95:
|
||||||
|
id: "_"
|
||||||
|
title: Underscore
|
||||||
|
123:
|
||||||
|
id: "{"
|
||||||
|
title: Left Curly Brace
|
||||||
|
124:
|
||||||
|
id: "|"
|
||||||
|
title: Pipe
|
||||||
|
125:
|
||||||
|
id: "}"
|
||||||
|
title: Right Curly Brace
|
||||||
|
126:
|
||||||
|
id: "~"
|
||||||
|
title: Tilde
|
||||||
@@ -7,39 +7,9 @@ actions:
|
|||||||
description: |
|
description: |
|
||||||
While SPACE is used for keymaps and chord, just a " " is used in chord outputs.
|
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.
|
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:
|
39:
|
||||||
id: "'"
|
id: "'"
|
||||||
title: Single Quote
|
title: Single Quote
|
||||||
40:
|
|
||||||
id: "("
|
|
||||||
title: Opening Parenthesis
|
|
||||||
41:
|
|
||||||
id: ")"
|
|
||||||
title: Closing Parenthesis
|
|
||||||
42:
|
|
||||||
id: "*"
|
|
||||||
title: Asterisk
|
|
||||||
43:
|
|
||||||
id: "+"
|
|
||||||
title: Plus
|
|
||||||
44:
|
44:
|
||||||
id: ","
|
id: ","
|
||||||
title: Comma
|
title: Comma
|
||||||
@@ -82,105 +52,12 @@ actions:
|
|||||||
57:
|
57:
|
||||||
id: "9"
|
id: "9"
|
||||||
title: Nine
|
title: Nine
|
||||||
58:
|
|
||||||
id: ":"
|
|
||||||
title: Colon
|
|
||||||
59:
|
59:
|
||||||
id: ";"
|
id: ";"
|
||||||
title: Semicolon
|
title: Semicolon
|
||||||
60:
|
|
||||||
id: "<"
|
|
||||||
title: Less Than
|
|
||||||
61:
|
61:
|
||||||
id: "="
|
id: "="
|
||||||
title: Equals
|
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:
|
91:
|
||||||
id: "["
|
id: "["
|
||||||
title: Left Bracket
|
title: Left Bracket
|
||||||
@@ -190,12 +67,6 @@ actions:
|
|||||||
93:
|
93:
|
||||||
id: "]"
|
id: "]"
|
||||||
title: Right Bracket
|
title: Right Bracket
|
||||||
94:
|
|
||||||
id: "^"
|
|
||||||
title: Caret
|
|
||||||
95:
|
|
||||||
id: "_"
|
|
||||||
title: Underscore
|
|
||||||
96:
|
96:
|
||||||
id: "`"
|
id: "`"
|
||||||
title: Backtick
|
title: Backtick
|
||||||
@@ -277,19 +148,6 @@ actions:
|
|||||||
122:
|
122:
|
||||||
id: "z"
|
id: "z"
|
||||||
title: Lowercase 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:
|
127:
|
||||||
id: "DEL"
|
id: "DEL"
|
||||||
title: Delete
|
title: Delete
|
||||||
icon: delete_forever
|
|
||||||
|
|||||||
@@ -6,34 +6,73 @@ type: unassigned
|
|||||||
actions:
|
actions:
|
||||||
600:
|
600:
|
||||||
id: "LH_THUMB_3_3D"
|
id: "LH_THUMB_3_3D"
|
||||||
title: Left Hand Thumb Top 3D Click
|
title: "Left Hand Thumb Bottom 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
601:
|
601:
|
||||||
id: "LH_THUMB_2_3D"
|
id: "LH_THUMB_2_3D"
|
||||||
title: Left Hand Thumb Middle 3D Click
|
title: "Left Hand Thumb Middle 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
602:
|
602:
|
||||||
id: "LH_THUMB_1_3D"
|
id: "LH_THUMB_1_3D"
|
||||||
title: Left Hand Thumb Bottom 3D Click
|
title: "Left Hand Thumb Top 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
603:
|
603:
|
||||||
id: "LH_INDEX_3D"
|
id: "LH_INDEX_3D"
|
||||||
title: Left Hand Index Finger 3D Click
|
title: "Left Hand Index Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
604:
|
604:
|
||||||
id: "LH_MID_1_3D"
|
id: "LH_MID_1_3D"
|
||||||
title: Left Hand Middle Finger 3D Click
|
title: "Left Hand Middle Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
605:
|
605:
|
||||||
id: "LH_RING_1_3D"
|
id: "LH_RING_1_3D"
|
||||||
title: Left Hand Ring Finger 3D Click
|
title: "Left Hand Ring Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
606:
|
606:
|
||||||
id: "LH_PINKY_3D"
|
id: "LH_PINKY_3D"
|
||||||
title: Left Hand Pinky 3D Click,
|
title: "Left Hand Pinky 3D Click"
|
||||||
# TODO...
|
icon: "adjust"
|
||||||
# ["607", "CharaChorder One", "LH_MID_2_3D", "", ""],
|
607:
|
||||||
# ["608", "CharaChorder One", "LH_RING_2_3D", "", ""],
|
id: "LH_MID_2_3D"
|
||||||
# ["609", "CharaChorder One", "RH_THUMB_3_3D", "", ""],
|
title: "Left Hand Middle Finger 2 3D Click"
|
||||||
# ["610", "CharaChorder One", "RH_THUMB_2_3D", "", ""],
|
icon: "adjust"
|
||||||
# ["611", "CharaChorder One", "RH_THUMB_1_3D", "", ""],
|
608:
|
||||||
# ["612", "CharaChorder One", "RH_INDEX_3D", "", ""],
|
id: "LH_RING_2_3D"
|
||||||
# ["613", "CharaChorder One", "RH_MID_1_3D", "", ""],
|
title: "Left Hand Ring Finger 2 3D Click"
|
||||||
# ["614", "CharaChorder One", "RH_RING_1_3D", "", ""],
|
icon: "adjust"
|
||||||
# ["615", "CharaChorder One", "RH_PINKY_3D", "", ""],
|
609:
|
||||||
# ["616", "CharaChorder One", "RH_MID_2_3D", "", ""],
|
id: "RH_THUMB_3_3D"
|
||||||
# ["617", "CharaChorder One", "RH_RING_2_3D", "", ""]
|
title: "Right Hand Thumb Bottom 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
610:
|
||||||
|
id: "RH_THUMB_2_3D"
|
||||||
|
title: "Right Hand Thumb Middle 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
611:
|
||||||
|
id: "RH_THUMB_1_3D"
|
||||||
|
title: "Right Hand Thumb Top 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
612:
|
||||||
|
id: "RH_INDEX_3D"
|
||||||
|
title: "Right Hand Index Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
613:
|
||||||
|
id: "RH_MID_1_3D"
|
||||||
|
title: "Right Hand Middle Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
614:
|
||||||
|
id: "RH_RING_1_3D"
|
||||||
|
title: "Right Hand Ring Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
615:
|
||||||
|
id: "RH_PINKY_3D"
|
||||||
|
title: "Right Hand Pinky 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
616:
|
||||||
|
id: "RH_MID_2_3D"
|
||||||
|
title: "Right Hand Middle Finger 2 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
617:
|
||||||
|
id: "RH_RING_2_3D"
|
||||||
|
title: "Right Hand Ring Finger 2 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ actions:
|
|||||||
536:
|
536:
|
||||||
id: "DUP"
|
id: "DUP"
|
||||||
title: Repeat Last Note
|
title: Repeat Last Note
|
||||||
icon: control_point_duplicate
|
icon: copy_all
|
||||||
description: |
|
description: |
|
||||||
In character entry, it repeats your last input.
|
In character entry, it repeats your last input.
|
||||||
In chorded entry, it is used for words with repeating letters.
|
In chorded entry, it is used for words with repeating letters.
|
||||||
@@ -91,3 +91,19 @@ actions:
|
|||||||
<<: *tertiary_keymap
|
<<: *tertiary_keymap
|
||||||
id: "KM_3_R"
|
id: "KM_3_R"
|
||||||
variant: right
|
variant: right
|
||||||
|
576:
|
||||||
|
id: ACTION_DELAY_1000
|
||||||
|
icon: clock_loader_90
|
||||||
|
description: Wait for one second
|
||||||
|
577:
|
||||||
|
id: ACTION_DELAY_100
|
||||||
|
icon: clock_loader_60
|
||||||
|
description: Wait for 100 milliseconds
|
||||||
|
578:
|
||||||
|
id: ACTION_DELAY_10
|
||||||
|
icon: clock_loader_40
|
||||||
|
description: Wait for 10 milliseconds
|
||||||
|
579:
|
||||||
|
id: ACTION_DELAY_1
|
||||||
|
icon: clock_loader_10
|
||||||
|
description: Wait for one millisecond
|
||||||
|
|||||||
@@ -4,41 +4,52 @@ icon: keyboard
|
|||||||
actions:
|
actions:
|
||||||
512: &left_ctrl
|
512: &left_ctrl
|
||||||
id: "LEFT_CTRL"
|
id: "LEFT_CTRL"
|
||||||
|
display: CTRL
|
||||||
title: Control Keyboard Modifier
|
title: Control Keyboard Modifier
|
||||||
|
keyCode: ControlLeft
|
||||||
variant: left
|
variant: left
|
||||||
icon: keyboard_control_key
|
|
||||||
513: &left_shift
|
513: &left_shift
|
||||||
id: "LEFT_SHIFT"
|
id: "LEFT_SHIFT"
|
||||||
title: Shift Keyboard Modifier
|
title: Shift Keyboard Modifier
|
||||||
|
keyCode: ShiftLeft
|
||||||
variant: left
|
variant: left
|
||||||
icon: shift
|
icon: shift
|
||||||
514: &left_alt
|
514: &left_alt
|
||||||
id: "LEFT_ALT"
|
id: "LEFT_ALT"
|
||||||
|
display: ALT
|
||||||
title: Alt Keyboard Modifier
|
title: Alt Keyboard Modifier
|
||||||
|
keyCode: AltLeft
|
||||||
variant: left
|
variant: left
|
||||||
icon: keyboard_option_key
|
|
||||||
515: &left_gui
|
515: &left_gui
|
||||||
id: "LEFT_GUI"
|
id: "LEFT_GUI"
|
||||||
title: GUI Keyboard Modifier
|
title: GUI Keyboard Modifier
|
||||||
|
keyCode: MetaLeft
|
||||||
|
icon: apps
|
||||||
variant: left
|
variant: left
|
||||||
icon: keyboard_command_key
|
|
||||||
516:
|
516:
|
||||||
variationOf: 512
|
variationOf: 512
|
||||||
<<: *left_ctrl
|
<<: *left_ctrl
|
||||||
id: "RIGHT_CTRL"
|
id: "RIGHT_CTRL"
|
||||||
|
keyCode: ControlRight
|
||||||
variant: right
|
variant: right
|
||||||
517:
|
517:
|
||||||
variationOf: 513
|
variationOf: 513
|
||||||
<<: *left_shift
|
<<: *left_shift
|
||||||
id: "RIGHT_SHIFT"
|
id: "RIGHT_SHIFT"
|
||||||
|
keyCode: ShiftRight
|
||||||
|
variant: right
|
||||||
518:
|
518:
|
||||||
variationOf: 514
|
variationOf: 514
|
||||||
<<: *left_alt
|
<<: *left_alt
|
||||||
id: "RIGHT_ALT"
|
id: "RIGHT_ALT"
|
||||||
|
keyCode: AltRight
|
||||||
|
variant: right
|
||||||
519:
|
519:
|
||||||
variationOf: 515
|
variationOf: 515
|
||||||
<<: *left_gui
|
<<: *left_gui
|
||||||
id: "RIGHT_GUI"
|
id: "RIGHT_GUI"
|
||||||
|
keyCode: MetaRight
|
||||||
|
variant: right
|
||||||
520:
|
520:
|
||||||
id: "RELEASE_MOD"
|
id: "RELEASE_MOD"
|
||||||
title: Release all keyboard modifiers
|
title: Release all keyboard modifiers
|
||||||
@@ -51,3 +62,11 @@ actions:
|
|||||||
id: "RELEASE_KEYS"
|
id: "RELEASE_KEYS"
|
||||||
title: Release all keys, but not keyboard modifiers
|
title: Release all keys, but not keyboard modifiers
|
||||||
icon: text_rotate_up
|
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
|
||||||
|
|||||||
25
src/lib/assets/keymaps/keymap.d.ts
vendored
25
src/lib/assets/keymaps/keymap.d.ts
vendored
@@ -1,16 +1,19 @@
|
|||||||
export interface KeymapCategory {
|
export interface KeymapCategory {
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
icon?: string
|
icon?: string;
|
||||||
type?: "unassigned"
|
display?: string;
|
||||||
actions: Record<number, Partial<ActionInfo>>
|
type?: "unassigned";
|
||||||
|
actions: Record<number, Partial<ActionInfo>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionInfo {
|
export interface ActionInfo {
|
||||||
id: string
|
id: string;
|
||||||
title: string
|
title: string;
|
||||||
icon: string
|
icon: string;
|
||||||
description: string
|
display: string;
|
||||||
variant: "left" | "right"
|
description: string;
|
||||||
variantOf: number
|
variant: "left" | "right";
|
||||||
|
variantOf: number;
|
||||||
|
keyCode: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
name: Raw Scancodes
|
name: Key codes
|
||||||
description: Raw Keyboard Scancodes
|
description: OS-Layout sensitive keycodes
|
||||||
actions:
|
actions:
|
||||||
256:
|
256:
|
||||||
id: "KSC_00"
|
id: "KSC_00"
|
||||||
|
icon: block
|
||||||
title: No Key Pressed
|
title: No Key Pressed
|
||||||
|
description: Also commonly used at the end of a chord to remove auto-spaces
|
||||||
257:
|
257:
|
||||||
id: "KSC_01"
|
id: "KSC_01"
|
||||||
title: Keyboard Error Roll Over
|
title: Keyboard Error Roll Over
|
||||||
@@ -15,311 +17,408 @@ actions:
|
|||||||
title: Keyboard Error Undefined
|
title: Keyboard Error Undefined
|
||||||
260:
|
260:
|
||||||
id: "KEY_A"
|
id: "KEY_A"
|
||||||
|
keyCode: "KeyA"
|
||||||
title: Keyboard a and A (US English)
|
title: Keyboard a and A (US English)
|
||||||
description: Non US English keyboard users may prefer these Raw Scancodes
|
description: Non US English keyboard users may prefer these Raw Scancodes
|
||||||
261:
|
261:
|
||||||
id: "KEY_B"
|
id: "KEY_B"
|
||||||
|
keyCode: "KeyB"
|
||||||
title: Keyboard b and B (US English)
|
title: Keyboard b and B (US English)
|
||||||
262:
|
262:
|
||||||
id: "KEY_C"
|
id: "KEY_C"
|
||||||
|
keyCode: "KeyC"
|
||||||
title: Keyboard c and C (US English)
|
title: Keyboard c and C (US English)
|
||||||
263:
|
263:
|
||||||
id: "KEY_D"
|
id: "KEY_D"
|
||||||
|
keyCode: "KeyD"
|
||||||
title: Keyboard d and D (US English)
|
title: Keyboard d and D (US English)
|
||||||
264:
|
264:
|
||||||
id: "KEY_E"
|
id: "KEY_E"
|
||||||
|
keyCode: "KeyE"
|
||||||
title: Keyboard e and E (US English)
|
title: Keyboard e and E (US English)
|
||||||
265:
|
265:
|
||||||
id: "KEY_F"
|
id: "KEY_F"
|
||||||
|
keyCode: "KeyF"
|
||||||
title: Keyboard f and F (US English)
|
title: Keyboard f and F (US English)
|
||||||
266:
|
266:
|
||||||
id: "KEY_G"
|
id: "KEY_G"
|
||||||
|
keyCode: "KeyG"
|
||||||
title: Keyboard g and G (US English)
|
title: Keyboard g and G (US English)
|
||||||
267:
|
267:
|
||||||
id: "KEY_H"
|
id: "KEY_H"
|
||||||
|
keyCode: "KeyH"
|
||||||
title: Keyboard h and H (US English)
|
title: Keyboard h and H (US English)
|
||||||
268:
|
268:
|
||||||
id: "KEY_I"
|
id: "KEY_I"
|
||||||
|
keyCode: "KeyI"
|
||||||
title: Keyboard i and I (US English)
|
title: Keyboard i and I (US English)
|
||||||
269:
|
269:
|
||||||
id: "KEY_J"
|
id: "KEY_J"
|
||||||
|
keyCode: "KeyJ"
|
||||||
title: Keyboard j and J (US English)
|
title: Keyboard j and J (US English)
|
||||||
270:
|
270:
|
||||||
id: "KEY_K"
|
id: "KEY_K"
|
||||||
|
keyCode: "KeyK"
|
||||||
title: Keyboard k and K (US English)
|
title: Keyboard k and K (US English)
|
||||||
271:
|
271:
|
||||||
id: "KEY_L"
|
id: "KEY_L"
|
||||||
|
keyCode: "KeyL"
|
||||||
title: Keyboard l and L (US English)
|
title: Keyboard l and L (US English)
|
||||||
272:
|
272:
|
||||||
id: "KEY_M"
|
id: "KEY_M"
|
||||||
|
keyCode: "KeyM"
|
||||||
title: Keyboard m and M (US English)
|
title: Keyboard m and M (US English)
|
||||||
273:
|
273:
|
||||||
id: "KEY_N"
|
id: "KEY_N"
|
||||||
|
keyCode: "KeyN"
|
||||||
title: Keyboard n and N (US English)
|
title: Keyboard n and N (US English)
|
||||||
274:
|
274:
|
||||||
id: "KEY_O"
|
id: "KEY_O"
|
||||||
|
keyCode: "KeyO"
|
||||||
title: Keyboard o and O (US English)
|
title: Keyboard o and O (US English)
|
||||||
275:
|
275:
|
||||||
id: "KEY_P"
|
id: "KEY_P"
|
||||||
|
keyCode: "KeyP"
|
||||||
title: Keyboard p and P (US English)
|
title: Keyboard p and P (US English)
|
||||||
276:
|
276:
|
||||||
id: "KEY_Q"
|
id: "KEY_Q"
|
||||||
|
keyCode: "KeyQ"
|
||||||
title: Keyboard q and Q (US English)
|
title: Keyboard q and Q (US English)
|
||||||
277:
|
277:
|
||||||
id: "KEY_R"
|
id: "KEY_R"
|
||||||
|
keyCode: "KeyR"
|
||||||
title: Keyboard r and R (US English)
|
title: Keyboard r and R (US English)
|
||||||
278:
|
278:
|
||||||
id: "KEY_S"
|
id: "KEY_S"
|
||||||
|
keyCode: "KeyS"
|
||||||
title: Keyboard s and S (US English)
|
title: Keyboard s and S (US English)
|
||||||
279:
|
279:
|
||||||
id: "KEY_T"
|
id: "KEY_T"
|
||||||
|
keyCode: "KeyT"
|
||||||
title: Keyboard t and T (US English)
|
title: Keyboard t and T (US English)
|
||||||
280:
|
280:
|
||||||
id: "KEY_U"
|
id: "KEY_U"
|
||||||
|
keyCode: "KeyU"
|
||||||
title: Keyboard u and U (US English)
|
title: Keyboard u and U (US English)
|
||||||
281:
|
281:
|
||||||
id: "KEY_V"
|
id: "KEY_V"
|
||||||
|
keyCode: "KeyV"
|
||||||
title: Keyboard v and V (US English)
|
title: Keyboard v and V (US English)
|
||||||
282:
|
282:
|
||||||
id: "KEY_W"
|
id: "KEY_W"
|
||||||
|
keyCode: "KeyW"
|
||||||
title: Keyboard w and W (US English)
|
title: Keyboard w and W (US English)
|
||||||
283:
|
283:
|
||||||
id: "KEY_X"
|
id: "KEY_X"
|
||||||
|
keyCode: "KeyX"
|
||||||
title: Keyboard x and X (US English)
|
title: Keyboard x and X (US English)
|
||||||
284:
|
284:
|
||||||
id: "KEY_Y"
|
id: "KEY_Y"
|
||||||
|
keyCode: "KeyY"
|
||||||
title: Keyboard y and Y (US English)
|
title: Keyboard y and Y (US English)
|
||||||
285:
|
285:
|
||||||
id: "KEY_Z"
|
id: "KEY_Z"
|
||||||
|
keyCode: "KeyZ"
|
||||||
title: Keyboard z and Z (US English)
|
title: Keyboard z and Z (US English)
|
||||||
286:
|
286:
|
||||||
id: "KEY_1"
|
id: "KEY_1"
|
||||||
|
keyCode: "Digit1"
|
||||||
title: Keyboard 1 and ! (US English)
|
title: Keyboard 1 and ! (US English)
|
||||||
287:
|
287:
|
||||||
id: "KEY_2"
|
id: "KEY_2"
|
||||||
|
keyCode: "Digit2"
|
||||||
title: Keyboard 2 and @ (US English)
|
title: Keyboard 2 and @ (US English)
|
||||||
288:
|
288:
|
||||||
id: "KEY_3"
|
id: "KEY_3"
|
||||||
|
keyCode: "Digit3"
|
||||||
title: Keyboard 3 and # (US English)
|
title: Keyboard 3 and # (US English)
|
||||||
289:
|
289:
|
||||||
id: "KEY_4"
|
id: "KEY_4"
|
||||||
|
keyCode: "Digit4"
|
||||||
title: Keyboard 4 and $ (US English)
|
title: Keyboard 4 and $ (US English)
|
||||||
290:
|
290:
|
||||||
id: "KEY_5"
|
id: "KEY_5"
|
||||||
|
keyCode: "Digit5"
|
||||||
title: Keyboard 5 and % (US English)
|
title: Keyboard 5 and % (US English)
|
||||||
291:
|
291:
|
||||||
id: "KEY_6"
|
id: "KEY_6"
|
||||||
|
keyCode: "Digit6"
|
||||||
title: Keyboard 6 and ^ (US English)
|
title: Keyboard 6 and ^ (US English)
|
||||||
292:
|
292:
|
||||||
id: "KEY_7"
|
id: "KEY_7"
|
||||||
|
keyCode: "Digit7"
|
||||||
title: Keyboard 7 and & (US English)
|
title: Keyboard 7 and & (US English)
|
||||||
293:
|
293:
|
||||||
id: "KEY_8"
|
id: "KEY_8"
|
||||||
|
keyCode: "Digit8"
|
||||||
title: Keyboard 8 and * (US English)
|
title: Keyboard 8 and * (US English)
|
||||||
294:
|
294:
|
||||||
id: "KEY_9"
|
id: "KEY_9"
|
||||||
|
keyCode: "Digit9"
|
||||||
title: Keyboard 9 and ( (US English)
|
title: Keyboard 9 and ( (US English)
|
||||||
295:
|
295:
|
||||||
id: "KEY_0"
|
id: "KEY_0"
|
||||||
|
keyCode: "Digit0"
|
||||||
title: Keyboard 0 and ) (US English)
|
title: Keyboard 0 and ) (US English)
|
||||||
296:
|
296:
|
||||||
id: "ENTER"
|
id: "ENTER"
|
||||||
|
keyCode: "Enter"
|
||||||
title: Keyboard Return (US English)
|
title: Keyboard Return (US English)
|
||||||
icon: keyboard_return
|
icon: keyboard_return
|
||||||
297:
|
297:
|
||||||
id: "ESC"
|
id: "ESC"
|
||||||
|
keyCode: "Escape"
|
||||||
title: Keyboard Escape (US English)
|
title: Keyboard Escape (US English)
|
||||||
298:
|
298:
|
||||||
id: "BKSP"
|
id: "BKSP"
|
||||||
|
keyCode: "Backspace"
|
||||||
title: Keyboard Backspace (US English)
|
title: Keyboard Backspace (US English)
|
||||||
icon: backspace
|
icon: backspace
|
||||||
299:
|
299:
|
||||||
id: "TAB"
|
id: "TAB"
|
||||||
|
keyCode: "Tab"
|
||||||
title: Keyboard Tab (US English)
|
title: Keyboard Tab (US English)
|
||||||
icon: keyboard_tab
|
icon: keyboard_tab
|
||||||
300:
|
300:
|
||||||
id: "KSC_2C"
|
id: "KSC_2C"
|
||||||
|
keyCode: "Space"
|
||||||
title: Keyboard Space (US English)
|
title: Keyboard Space (US English)
|
||||||
description: |
|
description: |
|
||||||
The ASCII space is preferred over this raw scancode for the space bar.
|
The ASCII space is preferred over this raw scancode for the space bar.
|
||||||
icon: space_bar
|
icon: space_bar
|
||||||
301:
|
301:
|
||||||
id: "KSC_2D"
|
id: "KSC_2D"
|
||||||
|
keyCode: "Minus"
|
||||||
title: Keyboard - and _ (US English)
|
title: Keyboard - and _ (US English)
|
||||||
302:
|
302:
|
||||||
id: "KSC_2E"
|
id: "KSC_2E"
|
||||||
|
keyCode: "Equal"
|
||||||
title: Keyboard = and + (US English)
|
title: Keyboard = and + (US English)
|
||||||
303:
|
303:
|
||||||
id: "KSC_2F"
|
id: "KSC_2F"
|
||||||
|
keyCode: "BracketLeft"
|
||||||
title: Keyboard [ and { (US English)
|
title: Keyboard [ and { (US English)
|
||||||
304:
|
304:
|
||||||
id: "KSC_30"
|
id: "KSC_30"
|
||||||
|
keyCode: "BracketRight"
|
||||||
title: Keyboard ] and } (US English)
|
title: Keyboard ] and } (US English)
|
||||||
305:
|
305:
|
||||||
id: "KSC_31"
|
id: "KSC_31"
|
||||||
|
keyCode: "Backslash"
|
||||||
title: Keyboard \ and | (US English)
|
title: Keyboard \ and | (US English)
|
||||||
306:
|
306:
|
||||||
id: "KSC_32"
|
id: "KSC_32"
|
||||||
|
# TODO: also backslash?
|
||||||
title: Keyboard Non-US \# and ~ (US English)
|
title: Keyboard Non-US \# and ~ (US English)
|
||||||
307:
|
307:
|
||||||
id: "KSC_33"
|
id: "KSC_33"
|
||||||
|
keyCode: "Semicolon"
|
||||||
title: "Keyboard ; and : (US English)"
|
title: "Keyboard ; and : (US English)"
|
||||||
308:
|
308:
|
||||||
id: "KSC_34"
|
id: "KSC_34"
|
||||||
|
keyCode: "Quote"
|
||||||
title: Keyboard ' and " (US English)
|
title: Keyboard ' and " (US English)
|
||||||
309:
|
309:
|
||||||
id: "KSC_35"
|
id: "KSC_35"
|
||||||
|
keyCode: "Backquote"
|
||||||
title: Keyboard ` and ~ (US English)
|
title: Keyboard ` and ~ (US English)
|
||||||
310:
|
310:
|
||||||
id: "KSC_36"
|
id: "KSC_36"
|
||||||
|
keyCode: "Comma"
|
||||||
title: Keyboard , and < (US English)
|
title: Keyboard , and < (US English)
|
||||||
311:
|
311:
|
||||||
id: "KSC_37"
|
id: "KSC_37"
|
||||||
|
keyCode: "Period"
|
||||||
title: Keyboard . and > (US English)
|
title: Keyboard . and > (US English)
|
||||||
312:
|
312:
|
||||||
id: "KSC_38"
|
id: "KSC_38"
|
||||||
|
keyCode: "Slash"
|
||||||
title: Keyboard / and ? (US English)
|
title: Keyboard / and ? (US English)
|
||||||
313:
|
313:
|
||||||
id: "CAPSLOCK"
|
id: "CAPSLOCK"
|
||||||
|
keyCode: "CapsLock"
|
||||||
title: Keyboard Caps Lock
|
title: Keyboard Caps Lock
|
||||||
icon: shift_lock
|
icon: shift_lock
|
||||||
314:
|
314:
|
||||||
id: "F1"
|
id: "F1"
|
||||||
|
keyCode: "F1"
|
||||||
title: Keyboard F1
|
title: Keyboard F1
|
||||||
315:
|
315:
|
||||||
id: "F2"
|
id: "F2"
|
||||||
|
keyCode: "F2"
|
||||||
title: Keyboard F2
|
title: Keyboard F2
|
||||||
316:
|
316:
|
||||||
id: "F3"
|
id: "F3"
|
||||||
|
keyCode: "F3"
|
||||||
title: Keyboard F3
|
title: Keyboard F3
|
||||||
317:
|
317:
|
||||||
id: "F4"
|
id: "F4"
|
||||||
|
keyCode: "F4"
|
||||||
title: Keyboard F4
|
title: Keyboard F4
|
||||||
318:
|
318:
|
||||||
id: "F5"
|
id: "F5"
|
||||||
|
keyCode: "F5"
|
||||||
title: Keyboard F5
|
title: Keyboard F5
|
||||||
319:
|
319:
|
||||||
id: "F6"
|
id: "F6"
|
||||||
|
keyCode: "F6"
|
||||||
title: Keyboard F6
|
title: Keyboard F6
|
||||||
320:
|
320:
|
||||||
id: "F7"
|
id: "F7"
|
||||||
|
keyCode: "F7"
|
||||||
title: Keyboard F7
|
title: Keyboard F7
|
||||||
321:
|
321:
|
||||||
id: "F8"
|
id: "F8"
|
||||||
|
keyCode: "F8"
|
||||||
title: Keyboard F8
|
title: Keyboard F8
|
||||||
322:
|
322:
|
||||||
id: "F9"
|
id: "F9"
|
||||||
|
keyCode: "F9"
|
||||||
title: Keyboard F9
|
title: Keyboard F9
|
||||||
323:
|
323:
|
||||||
id: "F10"
|
id: "F10"
|
||||||
|
keyCode: "F10"
|
||||||
title: Keyboard F10
|
title: Keyboard F10
|
||||||
324:
|
324:
|
||||||
id: "F11"
|
id: "F11"
|
||||||
|
keyCode: "F11"
|
||||||
title: Keyboard F11
|
title: Keyboard F11
|
||||||
325:
|
325:
|
||||||
id: "F12"
|
id: "F12"
|
||||||
|
keyCode: "F12"
|
||||||
title: Keyboard F12
|
title: Keyboard F12
|
||||||
326:
|
326:
|
||||||
id: "PRTSCN"
|
id: "PRTSCN"
|
||||||
|
keyCode: "PrintScreen"
|
||||||
title: Keyboard Print Screen
|
title: Keyboard Print Screen
|
||||||
icon: screenshot_monitor
|
icon: screenshot_monitor
|
||||||
327:
|
327:
|
||||||
id: "SCRLK"
|
id: "SCRLK"
|
||||||
|
keyCode: "ScrollLock"
|
||||||
title: Keyboard Scroll Lock
|
title: Keyboard Scroll Lock
|
||||||
328:
|
328:
|
||||||
id: "PAUSE"
|
id: "PAUSE"
|
||||||
|
keyCode: "Pause"
|
||||||
title: Keyboard Pause
|
title: Keyboard Pause
|
||||||
329:
|
329:
|
||||||
id: "INSERT"
|
id: "INSERT"
|
||||||
|
keyCode: "Insert"
|
||||||
title: Keyboard Insert
|
title: Keyboard Insert
|
||||||
icon: insert_text
|
icon: insert_text
|
||||||
330:
|
330:
|
||||||
id: "HOME"
|
id: "HOME"
|
||||||
|
keyCode: "Home"
|
||||||
title: Keyboard Home
|
title: Keyboard Home
|
||||||
icon: home
|
icon: home
|
||||||
331:
|
331:
|
||||||
id: "PGUP"
|
id: "PGUP"
|
||||||
|
keyCode: "PageUp"
|
||||||
title: Keyboard Page Up
|
title: Keyboard Page Up
|
||||||
icon: move_up
|
icon: move_up
|
||||||
332:
|
332:
|
||||||
id: "DELETE"
|
id: "DELETE"
|
||||||
|
keyCode: "Delete"
|
||||||
title: Keyboard Delete Forward
|
title: Keyboard Delete Forward
|
||||||
333:
|
333:
|
||||||
id: "END"
|
id: "END"
|
||||||
|
keyCode: "End"
|
||||||
title: Keyboard End
|
title: Keyboard End
|
||||||
334:
|
334:
|
||||||
id: "PGDN"
|
id: "PGDN"
|
||||||
|
keyCode: "PageDown"
|
||||||
title: Keyboard Page Down
|
title: Keyboard Page Down
|
||||||
icon: move_down
|
icon: move_down
|
||||||
335:
|
335:
|
||||||
id: "ARROW_RT"
|
id: "ARROW_RT"
|
||||||
|
keyCode: "ArrowRight"
|
||||||
title: Keyboard Right Arrow
|
title: Keyboard Right Arrow
|
||||||
icon: keyboard_arrow_right
|
icon: keyboard_arrow_right
|
||||||
336:
|
336:
|
||||||
id: "ARROW_LF"
|
id: "ARROW_LF"
|
||||||
|
keyCode: "ArrowLeft"
|
||||||
title: Keyboard Left Arrow
|
title: Keyboard Left Arrow
|
||||||
icon: keyboard_arrow_left
|
icon: keyboard_arrow_left
|
||||||
337:
|
337:
|
||||||
id: "ARROW_DN"
|
id: "ARROW_DN"
|
||||||
|
keyCode: "ArrowDown"
|
||||||
title: Keyboard Down Arrow
|
title: Keyboard Down Arrow
|
||||||
icon: keyboard_arrow_down
|
icon: keyboard_arrow_down
|
||||||
338:
|
338:
|
||||||
id: "ARROW_UP"
|
id: "ARROW_UP"
|
||||||
|
keyCode: "ArrowUp"
|
||||||
title: Keyboard Up Arrow
|
title: Keyboard Up Arrow
|
||||||
icon: keyboard_arrow_up
|
icon: keyboard_arrow_up
|
||||||
339:
|
339:
|
||||||
id: "NUMLOCK"
|
id: "NUMLOCK"
|
||||||
|
keyCode: "NumLock"
|
||||||
title: Keyboard Num Lock and Clear
|
title: Keyboard Num Lock and Clear
|
||||||
340:
|
340:
|
||||||
id: "KP_SLASH"
|
id: "KP_SLASH"
|
||||||
|
keyCode: "NumpadDivide"
|
||||||
title: Keypad /
|
title: Keypad /
|
||||||
341:
|
341:
|
||||||
id: "KP_ASTER"
|
id: "KP_ASTER"
|
||||||
|
keyCode: "NumpadStar"
|
||||||
title: Keypad *
|
title: Keypad *
|
||||||
342:
|
342:
|
||||||
id: "KP_MINUS"
|
id: "KP_MINUS"
|
||||||
|
keyCode: "NumpadSubtract"
|
||||||
title: Keypad -
|
title: Keypad -
|
||||||
343:
|
343:
|
||||||
id: "KP_PLUS"
|
id: "KP_PLUS"
|
||||||
|
keyCode: "NumpadAdd"
|
||||||
title: Keypad +
|
title: Keypad +
|
||||||
344:
|
344:
|
||||||
id: "KP_ENTER"
|
id: "KP_ENTER"
|
||||||
|
keyCode: "NumpadEnter"
|
||||||
title: Keypad Enter
|
title: Keypad Enter
|
||||||
345:
|
345:
|
||||||
id: "KP_1"
|
id: "KP_1"
|
||||||
|
keyCode: "Numpad1"
|
||||||
title: Keypad 1 and End
|
title: Keypad 1 and End
|
||||||
346:
|
346:
|
||||||
id: "KP_2"
|
id: "KP_2"
|
||||||
|
keyCode: "Numpad2"
|
||||||
title: Keypad 2 and Down Arrow
|
title: Keypad 2 and Down Arrow
|
||||||
347:
|
347:
|
||||||
id: "KP_3"
|
id: "KP_3"
|
||||||
|
keyCode: "Numpad3"
|
||||||
title: Keypad 3 and Page Down
|
title: Keypad 3 and Page Down
|
||||||
348:
|
348:
|
||||||
id: "KP_4"
|
id: "KP_4"
|
||||||
|
keyCode: "Numpad4"
|
||||||
title: Keypad 4 and Left Arrow
|
title: Keypad 4 and Left Arrow
|
||||||
349:
|
349:
|
||||||
id: "KP_5"
|
id: "KP_5"
|
||||||
|
keyCode: "Numpad5"
|
||||||
title: Keypad 5
|
title: Keypad 5
|
||||||
350:
|
350:
|
||||||
id: "KP_6"
|
id: "KP_6"
|
||||||
|
keyCode: "Numpad6"
|
||||||
title: Keypad 6 and Rigth Arrow
|
title: Keypad 6 and Rigth Arrow
|
||||||
351:
|
351:
|
||||||
id: "KP_7"
|
id: "KP_7"
|
||||||
|
keyCode: "Numpad7"
|
||||||
title: Keypad 7 and Home
|
title: Keypad 7 and Home
|
||||||
352:
|
352:
|
||||||
id: "KP_8"
|
id: "KP_8"
|
||||||
|
keyCode: "Numpad8"
|
||||||
title: Keypad 8 and Up Arrow
|
title: Keypad 8 and Up Arrow
|
||||||
353:
|
353:
|
||||||
id: "KP_9"
|
id: "KP_9"
|
||||||
|
keyCode: "Numpad9"
|
||||||
title: Keypad 9 and Page Up
|
title: Keypad 9 and Page Up
|
||||||
354:
|
354:
|
||||||
id: "KP_0"
|
id: "KP_0"
|
||||||
|
keyCode: "Numpad0"
|
||||||
title: Keypad 0 and Insert
|
title: Keypad 0 and Insert
|
||||||
355:
|
355:
|
||||||
id: "KP_DOT"
|
id: "KP_DOT"
|
||||||
|
keyCode: "NumpadDecimal"
|
||||||
title: Keypad . and Delete
|
title: Keypad . and Delete
|
||||||
356:
|
356:
|
||||||
id: "KSC_64"
|
id: "KSC_64"
|
||||||
|
keyCode: "IntlBackslash"
|
||||||
title: Keyboard Non-US \ and | (US English)
|
title: Keyboard Non-US \ and | (US English)
|
||||||
357:
|
357:
|
||||||
id: "COMPOSE"
|
id: "COMPOSE"
|
||||||
@@ -327,10 +426,12 @@ actions:
|
|||||||
description: Officially supported by Win, Unix, and Boot
|
description: Officially supported by Win, Unix, and Boot
|
||||||
358:
|
358:
|
||||||
id: "POWER"
|
id: "POWER"
|
||||||
|
keyCode: "Power"
|
||||||
title: Keyboard Power
|
title: Keyboard Power
|
||||||
description: Only officially supported by Mac and Unix
|
description: Only officially supported by Mac and Unix
|
||||||
359:
|
359:
|
||||||
id: "KP_EQUAL"
|
id: "KP_EQUAL"
|
||||||
|
keyCode: "NumpadEqual"
|
||||||
title: Keypad =
|
title: Keypad =
|
||||||
description: Only officially supported by Mac
|
description: Only officially supported by Mac
|
||||||
360:
|
360:
|
||||||
@@ -787,10 +888,12 @@ actions:
|
|||||||
description: Not required to be supported by any OS
|
description: Not required to be supported by any OS
|
||||||
472:
|
472:
|
||||||
id: "KSC_D8"
|
id: "KSC_D8"
|
||||||
|
keyCode: "NumpadClear"
|
||||||
title: Keypad Clear
|
title: Keypad Clear
|
||||||
description: Not required to be supported by any OS
|
description: Not required to be supported by any OS
|
||||||
473:
|
473:
|
||||||
id: "KSC_D9"
|
id: "KSC_D9"
|
||||||
|
keyCode: "NumpadClearEntry"
|
||||||
title: Keypad Clear Entry
|
title: Keypad Clear Entry
|
||||||
description: Not required to be supported by any OS
|
description: Not required to be supported by any OS
|
||||||
474:
|
474:
|
||||||
@@ -841,34 +944,42 @@ actions:
|
|||||||
title: Keyboard Right GUI
|
title: Keyboard Right GUI
|
||||||
488:
|
488:
|
||||||
id: "KSC_E8"
|
id: "KSC_E8"
|
||||||
|
keyCode: "MediaPlayPause"
|
||||||
title: Media Play Pause
|
title: Media Play Pause
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
489:
|
489:
|
||||||
id: "KSC_E9"
|
id: "KSC_E9"
|
||||||
|
keyCode: "MediaStop"
|
||||||
title: Media Stop CD
|
title: Media Stop CD
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
490:
|
490:
|
||||||
id: "KSC_EA"
|
id: "KSC_EA"
|
||||||
|
keyCode: "MediaTrackPrevious"
|
||||||
title: Media Previous Song
|
title: Media Previous Song
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
491:
|
491:
|
||||||
id: "KSC_EB"
|
id: "KSC_EB"
|
||||||
|
keyCode: "MediaTrackNext"
|
||||||
title: Media Next Song
|
title: Media Next Song
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
492:
|
492:
|
||||||
id: "KSC_EC"
|
id: "KSC_EC"
|
||||||
|
keyCode: "Eject"
|
||||||
title: Media Eject CD
|
title: Media Eject CD
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
493:
|
493:
|
||||||
id: "KSC_ED"
|
id: "KSC_ED"
|
||||||
|
keyCode: "AudioVolumeUp"
|
||||||
title: Media Volume Up
|
title: Media Volume Up
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
494:
|
494:
|
||||||
id: "KSC_EE"
|
id: "KSC_EE"
|
||||||
|
keyCode: "AudioVolumeDown"
|
||||||
title: Media Volume Down
|
title: Media Volume Down
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
495:
|
495:
|
||||||
id: "KSC_EF"
|
id: "KSC_EF"
|
||||||
|
keyCode: "AudioVolumeMute"
|
||||||
title: Media Mute
|
title: Media Mute
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
496:
|
496:
|
||||||
@@ -877,18 +988,22 @@ actions:
|
|||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
497:
|
497:
|
||||||
id: "KSC_F1"
|
id: "KSC_F1"
|
||||||
|
keyCode: "BrowserBack"
|
||||||
title: Media Back
|
title: Media Back
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
498:
|
498:
|
||||||
id: "KSC_F2"
|
id: "KSC_F2"
|
||||||
|
keyCode: "BrowserForward"
|
||||||
title: Media Forward
|
title: Media Forward
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
499:
|
499:
|
||||||
id: "KSC_F3"
|
id: "KSC_F3"
|
||||||
|
keyCode: "BrowserStop"
|
||||||
title: Media Stop
|
title: Media Stop
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
500:
|
500:
|
||||||
id: "KSC_F4"
|
id: "KSC_F4"
|
||||||
|
keyCode: "BrowserSearch"
|
||||||
title: Media Find
|
title: Media Find
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
501:
|
501:
|
||||||
@@ -905,14 +1020,17 @@ actions:
|
|||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
504:
|
504:
|
||||||
id: "KSC_F8"
|
id: "KSC_F8"
|
||||||
|
keyCode: "Sleep"
|
||||||
title: Media Sleep
|
title: Media Sleep
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
505:
|
505:
|
||||||
id: "KSC_F9"
|
id: "KSC_F9"
|
||||||
|
keyCode: "WakeUp"
|
||||||
title: Media Coffee
|
title: Media Coffee
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
506:
|
506:
|
||||||
id: "KSC_FA"
|
id: "KSC_FA"
|
||||||
|
keyCode: "BrowserRefresh"
|
||||||
title: Media Refresh
|
title: Media Refresh
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
507:
|
507:
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
name: CC1
|
|
||||||
col:
|
|
||||||
- gap: 156
|
|
||||||
row:
|
|
||||||
- row:
|
|
||||||
- {d: 30, e: 31, n: 32, w: 33, s: 34}
|
|
||||||
- col:
|
|
||||||
- {d: 25, e: 26, n: 27, w: 28, s: 29}
|
|
||||||
- {d: 40, e: 41, n: 42, w: 43, s: 44}
|
|
||||||
- col:
|
|
||||||
- {d: 20, e: 21, n: 22, w: 23, s: 24}
|
|
||||||
- {d: 35, e: 36, n: 37, w: 38, s: 39}
|
|
||||||
- {d: 15, e: 16, n: 17, w: 18, s: 19}
|
|
||||||
- row:
|
|
||||||
- {d: 60, w: 61, n: 62, e: 63, s: 64}
|
|
||||||
- col:
|
|
||||||
- {d: 65, w: 66, n: 67, e: 68, s: 69}
|
|
||||||
- {d: 80, w: 81, n: 82, e: 83, s: 84}
|
|
||||||
- col:
|
|
||||||
- {d: 70, w: 71, n: 72, e: 73, s: 74}
|
|
||||||
- {d: 85, w: 86, n: 87, e: 88, s: 89}
|
|
||||||
- {d: 75, w: 76, n: 77, e: 78, s: 79}
|
|
||||||
- gap: 48
|
|
||||||
margin-top: -32
|
|
||||||
row:
|
|
||||||
- {d: 10, e: 11, n: 12, w: 13, s: 14}
|
|
||||||
- {d: 55, w: 56, n: 57, e: 58, s: 59}
|
|
||||||
- gap: 160
|
|
||||||
row:
|
|
||||||
- {d: 5, e: 6, n: 7, w: 8, s: 9}
|
|
||||||
- {d: 50, w: 51, n: 52, e: 53, s: 54}
|
|
||||||
- gap: 320
|
|
||||||
margin-top: -12
|
|
||||||
row:
|
|
||||||
- {d: 0, e: 1, n: 2, w: 3, s: 4}
|
|
||||||
- {d: 45, w: 46, n: 47, e: 48, s: 49}
|
|
||||||
142
src/lib/assets/layouts/generic/103-key.yml
Normal file
142
src/lib/assets/layouts/generic/103-key.yml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
name: 103-key
|
||||||
|
col:
|
||||||
|
- row:
|
||||||
|
- 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
|
||||||
|
- key: 31
|
||||||
|
- key: 32
|
||||||
|
- key: 33
|
||||||
|
- key: 34
|
||||||
|
- key: 35
|
||||||
|
- key: 36
|
||||||
|
- key: 37
|
||||||
|
- key: 38
|
||||||
|
- key: 39
|
||||||
|
- 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: 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: 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: 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
|
||||||
86
src/lib/assets/layouts/lite.yml
Normal file
86
src/lib/assets/layouts/lite.yml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: Lite
|
||||||
|
col:
|
||||||
|
- row:
|
||||||
|
- key: 53
|
||||||
|
- key: 54
|
||||||
|
- key: 55
|
||||||
|
- key: 56
|
||||||
|
- key: 57
|
||||||
|
- key: 58
|
||||||
|
- key: 59
|
||||||
|
- key: 60
|
||||||
|
- key: 61
|
||||||
|
- key: 62
|
||||||
|
- key: 63
|
||||||
|
- key: 64
|
||||||
|
- key: 65
|
||||||
|
- key: 66
|
||||||
|
size: [2, 1]
|
||||||
|
- row:
|
||||||
|
- key: 39
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 40
|
||||||
|
- key: 41
|
||||||
|
- key: 42
|
||||||
|
- key: 43
|
||||||
|
- key: 44
|
||||||
|
- key: 45
|
||||||
|
- key: 46
|
||||||
|
- key: 47
|
||||||
|
- key: 48
|
||||||
|
- key: 49
|
||||||
|
- key: 50
|
||||||
|
- key: 51
|
||||||
|
- key: 52
|
||||||
|
size: [1.5, 1]
|
||||||
|
- row:
|
||||||
|
- key: 26
|
||||||
|
size: [1.75, 1]
|
||||||
|
- key: 27
|
||||||
|
- key: 28
|
||||||
|
- key: 29
|
||||||
|
- key: 30
|
||||||
|
- key: 31
|
||||||
|
- key: 32
|
||||||
|
- key: 33
|
||||||
|
- key: 34
|
||||||
|
- key: 35
|
||||||
|
- key: 36
|
||||||
|
- key: 37
|
||||||
|
- key: 38
|
||||||
|
size: [2.25, 1]
|
||||||
|
- row:
|
||||||
|
- key: 12
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 13
|
||||||
|
- key: 14
|
||||||
|
- key: 15
|
||||||
|
- key: 16
|
||||||
|
- key: 17
|
||||||
|
- key: 18
|
||||||
|
- key: 19
|
||||||
|
- key: 20
|
||||||
|
- key: 21
|
||||||
|
- key: 22
|
||||||
|
- key: 23
|
||||||
|
- key: 24
|
||||||
|
- key: 25
|
||||||
|
- row:
|
||||||
|
- key: 0
|
||||||
|
- key: 1
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 2
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 3
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 4
|
||||||
|
- key: 5
|
||||||
|
- key: 6
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 7
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 8
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 9
|
||||||
|
- key: 10
|
||||||
|
- key: 11
|
||||||
42
src/lib/assets/layouts/one.yml
Normal file
42
src/lib/assets/layouts/one.yml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: CC1
|
||||||
|
col:
|
||||||
|
# Ring / Middle
|
||||||
|
- offset: [2, 0]
|
||||||
|
row:
|
||||||
|
- switch: { d: 25, e: 26, n: 27, w: 28, s: 29 }
|
||||||
|
- switch: { d: 20, e: 21, n: 22, w: 23, s: 24 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { d: 65, w: 66, n: 67, e: 68, s: 69 }
|
||||||
|
- switch: { d: 70, w: 71, n: 72, e: 73, s: 74 }
|
||||||
|
- offset: [2, 0]
|
||||||
|
row:
|
||||||
|
- switch: { d: 40, e: 41, n: 42, w: 43, s: 44 }
|
||||||
|
- switch: { d: 35, e: 36, n: 37, w: 38, s: 39 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { d: 80, w: 81, n: 82, e: 83, s: 84 }
|
||||||
|
- switch: { d: 85, w: 86, n: 87, e: 88, s: 89 }
|
||||||
|
# Pinkie / Index
|
||||||
|
- offset: [0, -3]
|
||||||
|
row:
|
||||||
|
- switch: { d: 30, e: 31, n: 32, w: 33, s: 34 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { d: 15, e: 16, n: 17, w: 18, s: 19 }
|
||||||
|
- switch: { d: 60, w: 61, n: 62, e: 63, s: 64 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { d: 75, w: 76, n: 77, e: 78, s: 79 }
|
||||||
|
# Thumbs
|
||||||
|
- row:
|
||||||
|
- offset: [5.5, 0.5]
|
||||||
|
switch: { d: 10, e: 11, n: 12, w: 13, s: 14 }
|
||||||
|
- offset: [1, 0.5]
|
||||||
|
switch: { d: 55, w: 56, n: 57, e: 58, s: 59 }
|
||||||
|
- row:
|
||||||
|
- offset: [4.5, -0.25]
|
||||||
|
switch: { d: 5, e: 6, n: 7, w: 8, s: 9 }
|
||||||
|
- offset: [3, -0.25]
|
||||||
|
switch: { d: 50, w: 51, n: 52, e: 53, s: 54 }
|
||||||
|
- row:
|
||||||
|
- offset: [3.5, -0.25]
|
||||||
|
switch: { d: 0, e: 1, n: 2, w: 3, s: 4 }
|
||||||
|
- offset: [5, -0.25]
|
||||||
|
switch: { d: 45, w: 46, n: 47, e: 48, s: 49 }
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<mask id="cross" maskUnits="userSpaceOnUse">
|
|
||||||
<rect x="0" y="0" width="32" height="32" fill="white" />
|
|
||||||
<path d="M0 0L32 32M0 32L32 0" stroke="black" stroke-width="3" />
|
|
||||||
</mask>
|
|
||||||
<circle cx="16" cy="16" r="11.5" fill="none" stroke="white" stroke-width="9" mask="url(#cross)" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 433 B |
36
src/lib/assets/random-tips/en.json
Normal file
36
src/lib/assets/random-tips/en.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[
|
||||||
|
"You can use DUP+i to create chords on the fly in any text box",
|
||||||
|
"This site is open source! Check out the full source code on GitHub in the bottom left",
|
||||||
|
"Two letter chords can be activated accidentally in chentry. Be cautious around them",
|
||||||
|
"More inputs in a chord increase the tolerance, making them easier to activate",
|
||||||
|
"The maximum number of outputs in a chord is 256",
|
||||||
|
"You can create backups of your device on the top right",
|
||||||
|
"For programming you should set your auto-delete timeout to about 200ms",
|
||||||
|
"Large parts of this site were written on a CC1",
|
||||||
|
"I use VIM btw...",
|
||||||
|
"I use NixOS btw...",
|
||||||
|
"You can hold shift on the undo button to undo all changes",
|
||||||
|
"GTM stands for Generative Text Menu and can be used to change your device's settings anywhere",
|
||||||
|
"Ambidextrous Throwover (aka Mirror Mode) is a mode designed for one-handed use",
|
||||||
|
"Chentry stands for character entry, or typing letter by letter on a chording enabled device",
|
||||||
|
"Chord modifiers are hard-coded (as of now) and can be used in the English language to add conjucations and more",
|
||||||
|
"You can use 'cursor warping' by adding arrow key actions to a chord, for example to chord '()' with the cursor in the middle of the brackets",
|
||||||
|
"An arpeggiate is a single key press that modifies the preceding chord. Modifiers can be arpeggiated",
|
||||||
|
"Some actions are marked as a 'macro', which means the output is generated by a key sequence rather than a pure key press. Be cautious with those, as they can affect other keys when held together!",
|
||||||
|
"Spurring is a chording only mode which is more advanced, but can greatly imporve typing speed when mastered",
|
||||||
|
"The forced chord phenomenon is when typing a word character by character starts to feel unnatural",
|
||||||
|
"Don't be afraid to delete chords you keep getting wrong",
|
||||||
|
"Most people find it easier to start their chord library from scratch rather than learning someone else's",
|
||||||
|
"A common techinque to deal with conflicts is to add DUP or the same key mirrored on the other hand",
|
||||||
|
"A longer chord is not always more difficult",
|
||||||
|
"Riley Keen made headlines when his Monkeytype score of 500WPM using a CC1 got him banned off the site",
|
||||||
|
"A 3d press refers to pressing down into a 5-way switch",
|
||||||
|
"The serial communication protocol used by CCOS is documented on docs.charachorder.com",
|
||||||
|
"The 'CCOS is ready' message can be turned off in the settings",
|
||||||
|
"Most people using the CC1 don't change the a-z key layout, as further modification provides very little benefit",
|
||||||
|
"Using VIM on the default CC1 a-z layout is perfectly doable, it's just a matter of getting used to it",
|
||||||
|
"You can use Nexus to track words you might want to add to your chord library",
|
||||||
|
"The CC1 default layout was 80% science, 20% art",
|
||||||
|
"There is little to no reason to use hjkl in VIM on a CC1 since the arrows keys are so close already",
|
||||||
|
"The device manager automatically creates a backup for you when you reboot your device into the bootloader"
|
||||||
|
]
|
||||||
118
src/lib/assets/settings.yml
Normal file
118
src/lib/assets/settings.yml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
settings:
|
||||||
|
0x1:
|
||||||
|
title: Enable Serial Header
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
0x2:
|
||||||
|
title: Enable Serial Logging
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
0x3:
|
||||||
|
title: Enable Serial Debugging
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
0x4:
|
||||||
|
title: Enable Serial Raw
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
0x5:
|
||||||
|
title: Enable Serial Chord
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
0x6:
|
||||||
|
title: Enable Serial Keyboard
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
0x7:
|
||||||
|
title: Enable Serial Mouse
|
||||||
|
description: boolean 0 or 1, default is 0
|
||||||
|
0x11:
|
||||||
|
title: Enable USB HID Keyboard
|
||||||
|
description: boolean 0 or 1, default is 1
|
||||||
|
0x12:
|
||||||
|
title: Enable Character Entry
|
||||||
|
description: boolean 0 or 1
|
||||||
|
0x13:
|
||||||
|
title: GUI-CTRL Swap Mode
|
||||||
|
description: boolean 0 or 1; 1 swaps keymap 0 and 1. (CCL only)
|
||||||
|
0x14:
|
||||||
|
title: Key Scan Duration
|
||||||
|
description: scan rate described in milliseconds; default is 2ms = 500Hz
|
||||||
|
0x15:
|
||||||
|
title: Key Debounce Press Duration
|
||||||
|
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
||||||
|
0x16:
|
||||||
|
title: Key Debounce Release Duration
|
||||||
|
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
|
||||||
|
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
|
||||||
|
0x21:
|
||||||
|
title: Enable USB HID Mouse
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
0x22:
|
||||||
|
title: Slow Mouse Speed
|
||||||
|
description: pixels to move at the mouse poll rate; default for CC1 is 5 = 250px/s
|
||||||
|
0x23:
|
||||||
|
title: Fast Mouse Speed
|
||||||
|
description: pixels to move at the mouse poll rate; default for CC1 is 25 = 1250px/s
|
||||||
|
0x24:
|
||||||
|
title: Enable Active Mouse
|
||||||
|
description: boolean 0 or 1; moves mouse back and forth every 60s
|
||||||
|
0x25:
|
||||||
|
title: Mouse Scroll Speed
|
||||||
|
description: default is 1; polls at 1/4th the rate of the mouse move updates
|
||||||
|
0x26:
|
||||||
|
title: Mouse Poll Duration
|
||||||
|
description: poll rate described in milliseconds; default is 20ms = 50Hz
|
||||||
|
0x31:
|
||||||
|
title: Enable Chording
|
||||||
|
description: boolean 0 or 1
|
||||||
|
0x32:
|
||||||
|
title: Enable Chording Character Counter Timeout
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
0x33:
|
||||||
|
title: Chording Character Counter Timeout Timer
|
||||||
|
description: 0-255 deciseconds; default is 40 or 4.0 seconds
|
||||||
|
0x34:
|
||||||
|
title: Chord Detection Press Tolerance(ms)
|
||||||
|
description: 1-50 milliseconds
|
||||||
|
0x35:
|
||||||
|
title: Chord Detection Release Tolerance(ms)
|
||||||
|
description: 1-50 milliseconds
|
||||||
|
0x41:
|
||||||
|
title: Enable Spurring
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
0x42:
|
||||||
|
title: Enable Spurring Character Counter Timeout
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
0x43:
|
||||||
|
title: Spurring Character Counter Timeout Timer
|
||||||
|
description: 0-255 seconds; default is 240
|
||||||
|
0x51:
|
||||||
|
title: Enable Arpeggiates
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
0x54:
|
||||||
|
title: Arpeggiate Tolerance
|
||||||
|
description: in milliseconds; default 800ms
|
||||||
|
0x61:
|
||||||
|
title: Enable Compound Chording (coming soon)
|
||||||
|
description: boolean 0 or 1; default is 0
|
||||||
|
0x64:
|
||||||
|
title: Compound Tolerance
|
||||||
|
description: in milliseconds; default 1500ms
|
||||||
|
0x81:
|
||||||
|
title: LED Brightness
|
||||||
|
description: 0-50 (CCL only); default is 5, which draws around 100 mA of current
|
||||||
|
0x82:
|
||||||
|
title: LED Color Code
|
||||||
|
description: Color Codes to be listed (CCL only)
|
||||||
|
0x83:
|
||||||
|
title: Enable LED Key Highlight (coming soon)
|
||||||
|
description: boolean 0 or 1 (CCL only)
|
||||||
|
0x84:
|
||||||
|
title: Enable LEDs
|
||||||
|
description: boolean 0 or 1; default is 1 (CCL only)
|
||||||
|
0x91:
|
||||||
|
title: Operating System
|
||||||
|
description: Operating system codes listed below
|
||||||
|
0x92:
|
||||||
|
title: Enable Realtime Feedback
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
|
0x93:
|
||||||
|
title: Enable CharaChorder Ready on startup
|
||||||
|
description: boolean 0 or 1; default is 1
|
||||||
192
src/lib/backup/backup.ts
Normal file
192
src/lib/backup/backup.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import type {
|
||||||
|
CharaBackupFile,
|
||||||
|
CharaChordFile,
|
||||||
|
CharaFile,
|
||||||
|
CharaLayoutFile,
|
||||||
|
CharaSettingsFile,
|
||||||
|
} from "$lib/share/chara-file.js";
|
||||||
|
import type { Change } from "$lib/undo-redo.js";
|
||||||
|
import {
|
||||||
|
changes,
|
||||||
|
ChangeType,
|
||||||
|
chords,
|
||||||
|
layout,
|
||||||
|
settings,
|
||||||
|
} from "$lib/undo-redo.js";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import { serialPort } from "../serial/connection";
|
||||||
|
import { csvLayoutToJson, isCsvLayout } from "$lib/backup/compat/legacy-layout";
|
||||||
|
import { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
|
||||||
|
|
||||||
|
export function downloadFile<T extends CharaFile<string>>(contents: T) {
|
||||||
|
const downloadUrl = URL.createObjectURL(
|
||||||
|
new Blob([JSON.stringify(contents)], { type: "application/json" }),
|
||||||
|
);
|
||||||
|
const element = document.createElement("a");
|
||||||
|
element.setAttribute(
|
||||||
|
"download",
|
||||||
|
`${contents.type}-${
|
||||||
|
get(serialPort)?.device
|
||||||
|
}-${new Date().toISOString()}.json`,
|
||||||
|
);
|
||||||
|
element.href = downloadUrl;
|
||||||
|
element.setAttribute("target", "_blank");
|
||||||
|
element.click();
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadBackup() {
|
||||||
|
downloadFile<CharaBackupFile>({
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "backup",
|
||||||
|
history: [
|
||||||
|
[createChordBackup(), createLayoutBackup(), createSettingsBackup()],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLayoutBackup(): CharaLayoutFile {
|
||||||
|
return {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "layout",
|
||||||
|
device: get(serialPort)?.device,
|
||||||
|
layout: get(layout).map((it) => it.map((it) => it.action)) as [
|
||||||
|
number[],
|
||||||
|
number[],
|
||||||
|
number[],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChordBackup(): CharaChordFile {
|
||||||
|
return {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "chords",
|
||||||
|
chords: get(chords).map((it) => [it.actions, it.phrase]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSettingsBackup(): CharaSettingsFile {
|
||||||
|
return {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "settings",
|
||||||
|
settings: get(settings).map((it) => it.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreBackup(event: Event) {
|
||||||
|
const input = (event.target as HTMLInputElement).files![0];
|
||||||
|
if (!input) return;
|
||||||
|
const text = await input.text();
|
||||||
|
if (input.name.endsWith(".json")) {
|
||||||
|
restoreFromFile(JSON.parse(text));
|
||||||
|
} else if (isCsvLayout(text)) {
|
||||||
|
restoreFromFile(csvLayoutToJson(text));
|
||||||
|
} else if (isCsvChords(text)) {
|
||||||
|
restoreFromFile(csvChordsToJson(text));
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreFromFile(
|
||||||
|
file: CharaBackupFile | CharaSettingsFile | CharaLayoutFile | CharaChordFile,
|
||||||
|
) {
|
||||||
|
if (file.charaVersion !== 1) throw new Error("Incompatible backup");
|
||||||
|
switch (file.type) {
|
||||||
|
case "backup": {
|
||||||
|
const recent = file.history[0];
|
||||||
|
if (!recent) return;
|
||||||
|
if (recent[1].device !== get(serialPort)?.device) {
|
||||||
|
alert("Backup is incompatible with this device");
|
||||||
|
throw new Error("Backup is incompatible with this device");
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.update((changes) => {
|
||||||
|
changes.push(
|
||||||
|
...getChangesFromChordFile(recent[0]),
|
||||||
|
...getChangesFromLayoutFile(recent[1]),
|
||||||
|
...getChangesFromSettingsFile(recent[2]),
|
||||||
|
);
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "chords": {
|
||||||
|
changes.update((changes) => {
|
||||||
|
changes.push(...getChangesFromChordFile(file));
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "layout": {
|
||||||
|
changes.update((changes) => {
|
||||||
|
changes.push(...getChangesFromLayoutFile(file));
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "settings": {
|
||||||
|
changes.update((changes) => {
|
||||||
|
changes.push(...getChangesFromSettingsFile(file));
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(
|
||||||
|
`Unknown backup type "${(file as CharaFile<string>).type}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChangesFromChordFile(file: CharaChordFile) {
|
||||||
|
const changes: Change[] = [];
|
||||||
|
const existingChords = new Set(
|
||||||
|
get(chords).map(({ phrase, actions }) => JSON.stringify([actions, phrase])),
|
||||||
|
);
|
||||||
|
for (const [input, output] of file.chords) {
|
||||||
|
if (existingChords.has(JSON.stringify([input, output]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changes.push({
|
||||||
|
type: ChangeType.Chord,
|
||||||
|
actions: input,
|
||||||
|
phrase: output,
|
||||||
|
id: input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
||||||
|
const changes: Change[] = [];
|
||||||
|
for (const [id, value] of file.settings.entries()) {
|
||||||
|
const setting = get(settings)[id];
|
||||||
|
if (setting !== undefined && setting.value !== value) {
|
||||||
|
changes.push({
|
||||||
|
type: ChangeType.Setting,
|
||||||
|
id,
|
||||||
|
setting: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChangesFromLayoutFile(file: CharaLayoutFile) {
|
||||||
|
const changes: Change[] = [];
|
||||||
|
for (const [layer, keys] of file.layout.entries()) {
|
||||||
|
for (const [id, action] of keys.entries()) {
|
||||||
|
if (get(layout)[layer]?.[id]?.action !== action) {
|
||||||
|
changes.push({
|
||||||
|
type: ChangeType.Layout,
|
||||||
|
layer,
|
||||||
|
id,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
26
src/lib/backup/compat/legacy-chords.sample.csv
Normal file
26
src/lib/backup/compat/legacy-chords.sample.csv
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
e + b + a,babe
|
||||||
|
e + c + b,because
|
||||||
|
f + e + c + a,face
|
||||||
|
h + e + c + a,each
|
||||||
|
i + d + ',I'd
|
||||||
|
i + g + b,big
|
||||||
|
i + g + e,give
|
||||||
|
k + b + a,back
|
||||||
|
k + e + a,take
|
||||||
|
l + e + a,late
|
||||||
|
l + e + d + a,lead
|
||||||
|
l + f + e,feel
|
||||||
|
l + g + e + a,large
|
||||||
|
l + h + e,help
|
||||||
|
l + i + a,Lia
|
||||||
|
l + i + f,fill
|
||||||
|
l + i + f + e,life
|
||||||
|
l + i + g + b + a,gitlab
|
||||||
|
l + k + i + e,like
|
||||||
|
m + e + a,make
|
||||||
|
m + i + ',I'm
|
||||||
|
n + c + a,can
|
||||||
|
n + d + a,and
|
||||||
|
n + e + b,been
|
||||||
|
n + e + b + a,enable
|
||||||
|
n + e + d,end
|
||||||
|
26
src/lib/backup/compat/legacy-chords.sample.json
Normal file
26
src/lib/backup/compat/legacy-chords.sample.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
e + b + a,babe
|
||||||
|
e + c + b,because
|
||||||
|
f + e + c + a,face
|
||||||
|
h + e + c + a,each
|
||||||
|
i + d + ',I'd
|
||||||
|
i + g + b,big
|
||||||
|
i + g + e,give
|
||||||
|
k + b + a,back
|
||||||
|
k + e + a,take
|
||||||
|
l + e + a,late
|
||||||
|
l + e + d + a,lead
|
||||||
|
l + f + e,feel
|
||||||
|
l + g + e + a,large
|
||||||
|
l + h + e,help
|
||||||
|
l + i + a,Lia
|
||||||
|
l + i + f,fill
|
||||||
|
l + i + f + e,life
|
||||||
|
l + i + g + b + a,gitlab
|
||||||
|
l + k + i + e,like
|
||||||
|
m + e + a,make
|
||||||
|
m + i + ',I'm
|
||||||
|
n + c + a,can
|
||||||
|
n + d + a,and
|
||||||
|
n + e + b,been
|
||||||
|
n + e + b + a,enable
|
||||||
|
n + e + d,end
|
||||||
31
src/lib/backup/compat/legacy-chords.ts
Normal file
31
src/lib/backup/compat/legacy-chords.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
const [input, output] = line.split(/,(?=[^,]*$)/, 2);
|
||||||
|
return [
|
||||||
|
input!
|
||||||
|
.split("+")
|
||||||
|
.map((it) => KEYMAP_IDS.get(it.trim())?.code ?? 0)
|
||||||
|
.sort((a, b) => a - b),
|
||||||
|
output!
|
||||||
|
.trim()
|
||||||
|
.split("")
|
||||||
|
.map((it) => KEYMAP_IDS.get(SPECIAL_KEYS.get(it) ?? it)?.code ?? 0),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCsvChords(csv: string): boolean {
|
||||||
|
return /^([^+]+( *\+ *[^+]+)* *, *[^+, ]+ *(\n|(?=$)))+$/.test(csv);
|
||||||
|
}
|
||||||
27
src/lib/backup/compat/legacy-layout-converted.sample.json
Normal file
27
src/lib/backup/compat/legacy-layout-converted.sample.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "layout",
|
||||||
|
"device": "one",
|
||||||
|
"layout": [
|
||||||
|
[
|
||||||
|
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262,
|
||||||
|
288, 277, 298, 307, 264, 287, 268, 332, 311, 274, 286, 308, 329, 310, 280,
|
||||||
|
358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315, 314, 317, 316,
|
||||||
|
312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263,
|
||||||
|
293, 260, 296, 544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278,
|
||||||
|
357, 516, 519, 517, 518, 327, 336, 338, 335, 337, 328, 325, 322, 323, 324
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
18
src/lib/backup/compat/legacy-layout.spec.ts
Normal file
18
src/lib/backup/compat/legacy-layout.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import legacyLayout from "./legacy-layout.sample.csv?raw";
|
||||||
|
import legacyLayoutConverted from "./legacy-layout-converted.sample.json";
|
||||||
|
import { csvLayoutToJson, isCsvLayout } from "./legacy-layout";
|
||||||
|
|
||||||
|
describe("legacy layout", () => {
|
||||||
|
it("should detect a legacy layout", () => {
|
||||||
|
expect(isCsvLayout(legacyLayout)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not detect chord maps as layouts", () => {
|
||||||
|
expect(isCsvLayout("e + h + t,the")).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert legacy layouts", () => {
|
||||||
|
expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted);
|
||||||
|
});
|
||||||
|
});
|
||||||
28
src/lib/backup/compat/legacy-layout.ts
Normal file
28
src/lib/backup/compat/legacy-layout.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { CharaLayoutFile } from "$lib/share/chara-file";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a legacy CSV-based layout to the modern JSON-based format
|
||||||
|
*/
|
||||||
|
export function csvLayoutToJson(
|
||||||
|
csv: string,
|
||||||
|
device: CharaLayoutFile["device"] = "one",
|
||||||
|
): CharaLayoutFile {
|
||||||
|
const layout: CharaLayoutFile = {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "layout",
|
||||||
|
device,
|
||||||
|
layout: [[], [], []],
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCsvLayout(csv: string): boolean {
|
||||||
|
return /^(A[123],\d+,\d+\n?)+$/.test(csv);
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"charaVersion": 1,
|
|
||||||
"type": "layout",
|
|
||||||
"device": "one",
|
|
||||||
"layout": [
|
|
||||||
[
|
|
||||||
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262, 288, 277, 298, 307, 264, 287,
|
|
||||||
268, 332, 311, 274, 286, 308, 329, 310, 280, 358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315,
|
|
||||||
314, 317, 316, 312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263, 293, 260, 296,
|
|
||||||
544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278, 357, 516, 519, 517, 518, 327, 336, 338, 335,
|
|
||||||
337, 328, 325, 322, 323, 324
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import {describe, expect, it} from "vitest"
|
|
||||||
import legacyLayout from "./legacy-layout.sample.csv?raw"
|
|
||||||
import legacyLayoutConverted from "./legacy-layout-converted.sample.json"
|
|
||||||
import {csvLayoutToJson, isCsvLayout} from "./legacy-layout"
|
|
||||||
|
|
||||||
describe("legacy layout", () => {
|
|
||||||
it("should detect a legacy layout", () => {
|
|
||||||
expect(isCsvLayout(legacyLayout)).to.be.true
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not detect chord maps as layouts", () => {
|
|
||||||
expect(isCsvLayout("e + h + t,the")).to.be.false
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should convert legacy layouts", () => {
|
|
||||||
expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type {CharaLayoutFile} from "$lib/share/chara-file"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a legacy CSV-based layout to the modern JSON-based format
|
|
||||||
*/
|
|
||||||
export function csvLayoutToJson(csv: string, device: CharaLayoutFile["device"] = "one"): CharaLayoutFile {
|
|
||||||
const layout: CharaLayoutFile = {
|
|
||||||
charaVersion: 1,
|
|
||||||
type: "layout",
|
|
||||||
device,
|
|
||||||
layout: [[], [], []],
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const layer of csv.split("\n")) {
|
|
||||||
const [layerId, key, action] = layer.substring(1).split(",").map(Number)
|
|
||||||
|
|
||||||
layout.layout[Number(layerId) - 1][Number(key)] = Number(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCsvLayout(csv: string): boolean {
|
|
||||||
return /^(A[123],\d+,\d+\n?)+$/.test(csv)
|
|
||||||
}
|
|
||||||
97
src/lib/components/Action.svelte
Normal file
97
src/lib/components/Action.svelte
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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.get(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 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
|
||||||
|
class:left={info.variant === "left"}
|
||||||
|
class:right={info.variant === "right"}>{info.id}</span
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<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}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
kbd:not(.inline-kbd) {
|
||||||
|
height: 24px;
|
||||||
|
padding-block: auto;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(span) + .inline-kbd {
|
||||||
|
margin-inline-start: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
||||||
import type {KeyInfo} 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
|
export let id: number | KeyInfo;
|
||||||
|
|
||||||
$: key = (typeof id === "number" ? KEYMAP_CODES[id] ?? id : id) as number | KeyInfo
|
$: key = (typeof id === "number" ? KEYMAP_CODES.get(id) ?? id : id) as
|
||||||
|
| number
|
||||||
|
| KeyInfo;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button on:click>
|
<button on:click>
|
||||||
@@ -21,8 +25,14 @@
|
|||||||
{#if key.description}
|
{#if key.description}
|
||||||
<i>{key.description}</i>
|
<i>{key.description}</i>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
<span class:icon={!!key.icon} class="key">{key.icon || key.id || `0x${key.code.toString(16)}`}</span>
|
<Action display="keys" action={key} />
|
||||||
{:else}
|
{:else}
|
||||||
<span class="key">0x{key.toString(16)}</span>
|
<span class="key">0x{key.toString(16)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -35,20 +45,33 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
font-family: "Noto Sans Mono", monospace;
|
||||||
color: inherit;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
&:focus-visible {
|
@media not (forced-colors: active) {
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
color: inherit;
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
outline: none;
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
border: 1px solid ButtonBorder;
|
||||||
|
margin-block: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ActiveText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,17 +85,14 @@
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.key {
|
.warning {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 4px;
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
|
|
||||||
min-width: 32px;
|
> :global(.icon) {
|
||||||
padding: 4px;
|
font-size: 16px;
|
||||||
|
}
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
border: 1px solid currentcolor;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
11
src/lib/components/ActionString.svelte
Normal file
11
src/lib/components/ActionString.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Action from "$lib/components/Action.svelte";
|
||||||
|
import type { KeyInfo } from "$lib/serial/keymap-codes";
|
||||||
|
|
||||||
|
export let actions: Array<number | KeyInfo>;
|
||||||
|
export let display: "keys" | "inline-keys" = "inline-keys";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each actions as action, i (`${typeof action === "number" ? action : action.code}:${i}`)}
|
||||||
|
<Action {action} {display} />
|
||||||
|
{/each}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import {useRegisterSW} from "virtual:pwa-register/svelte"
|
// @ts-expect-error no types here
|
||||||
|
import { useRegisterSW } from "virtual:pwa-register/svelte";
|
||||||
|
|
||||||
const {needRefresh, updateServiceWorker, offlineReady} = useRegisterSW()
|
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $needRefresh}
|
{#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}
|
{:else if $offlineReady}
|
||||||
<div title="App can now be used offline" class="icon">offline_pin</div>
|
<div title="App can now be used offline" class="icon">offline_pin</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {serialLog, serialPort} from "$lib/serial/connection"
|
import { serialLog, serialPort } from "$lib/serial/connection";
|
||||||
import {slide} from "svelte/transition"
|
import { slide } from "svelte/transition";
|
||||||
|
|
||||||
function submit(event: Event) {
|
function submit(event: Event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
$serialPort.send(value.trim())
|
$serialPort?.send(0, value.trim());
|
||||||
value = ""
|
value = "";
|
||||||
io.scrollTo({top: io.scrollHeight})
|
io.scrollTo({ top: io.scrollHeight });
|
||||||
}
|
}
|
||||||
|
|
||||||
let value: string
|
let value: string;
|
||||||
let io: HTMLDivElement
|
let io: HTMLDivElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form on:submit={submit}>
|
<form on:submit={submit}>
|
||||||
<div bind:this={io} class="io">
|
<div bind:this={io} class="io">
|
||||||
{#each $serialLog as { type, value }}
|
{#each $serialLog as { type, value }}
|
||||||
<p class={type} transition:slide>{value}</p>
|
{#if type === "input"}
|
||||||
|
<code transition:slide>{value}</code>
|
||||||
|
{:else if type === "output"}
|
||||||
|
<samp transition:slide>{value}</samp>
|
||||||
|
{:else}
|
||||||
|
<p transition:slide>{value}</p>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<div class="anchor" />
|
<div class="anchor" />
|
||||||
</div>
|
</div>
|
||||||
@@ -111,17 +117,15 @@
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
samp,
|
||||||
p {
|
p {
|
||||||
|
display: block;
|
||||||
overflow-anchor: none;
|
overflow-anchor: none;
|
||||||
margin-block: 0.15rem;
|
margin-block: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.input {
|
p {
|
||||||
margin-block-end: 0.25rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.system {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
@@ -134,8 +138,9 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.input::before {
|
code::before {
|
||||||
content: "> ";
|
content: "> ";
|
||||||
|
margin-block-end: 0.25rem;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: var(--md-sys-color-primary);
|
color: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/lib/components/Tooltip.svelte
Normal file
22
src/lib/components/Tooltip.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let title: string | undefined;
|
||||||
|
export let shortcut: string | undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if title}
|
||||||
|
<p>{@html title}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if shortcut}
|
||||||
|
<kbd>
|
||||||
|
{#each shortcut.split("+") as key}
|
||||||
|
<kbd>{key}</kbd>
|
||||||
|
{/each}
|
||||||
|
</kbd>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
p {
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,111 +1,145 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {
|
||||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
KEYMAP_CATEGORIES,
|
||||||
import Index from "flexsearch"
|
KEYMAP_CODES,
|
||||||
import {createEventDispatcher} from "svelte"
|
KEYMAP_IDS,
|
||||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
} from "$lib/serial/keymap-codes";
|
||||||
import LL from "../../../i18n/i18n-svelte"
|
import FlexSearch from "flexsearch";
|
||||||
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
|
import ActionListItem from "$lib/components/ActionListItem.svelte";
|
||||||
|
import LL from "../../../i18n/i18n-svelte";
|
||||||
|
import { action } from "$lib/title";
|
||||||
|
|
||||||
export let currentAction: number
|
export let currentAction: number | undefined = undefined;
|
||||||
|
export let nextAction: number | undefined = undefined;
|
||||||
|
|
||||||
const index = new Index({tokenize: "full"})
|
onMount(() => {
|
||||||
for (const action of Object.values(KEYMAP_CODES)) {
|
searchBox.focus();
|
||||||
index?.add(
|
});
|
||||||
action.code,
|
|
||||||
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
const index = new FlexSearch.Index({ tokenize: "full" });
|
||||||
action.description || ""
|
createIndex();
|
||||||
}`,
|
|
||||||
)
|
async function createIndex() {
|
||||||
|
for (const [, action] of KEYMAP_CODES) {
|
||||||
|
await index?.addAsync(
|
||||||
|
action.code,
|
||||||
|
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
||||||
|
action.description || ""
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const exactIndex: Record<string, KeyInfo> = Object.fromEntries(
|
|
||||||
Object.values(KEYMAP_CODES)
|
|
||||||
.filter(it => !!it.id)
|
|
||||||
.map(it => [it.id, it] as const),
|
|
||||||
)
|
|
||||||
|
|
||||||
function search() {
|
async function search() {
|
||||||
results = index!.search(searchBox.value)
|
results = (await index!.searchAsync(searchBox.value)) as number[];
|
||||||
exact = exactIndex[searchBox.value]?.code
|
exact = KEYMAP_IDS.get(searchBox.value)?.code;
|
||||||
code = Number(searchBox.value)
|
code = Number(searchBox.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(id?: number) {
|
function select(id?: number) {
|
||||||
if (id !== undefined) {
|
if (id !== undefined) {
|
||||||
dispatch("select", id)
|
dispatch("select", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyboardNavigation(event: KeyboardEvent) {
|
function keyboardNavigation(event: KeyboardEvent) {
|
||||||
if (event.shiftKey && event.key === "Enter") {
|
if (event.shiftKey && event.key === "Enter") {
|
||||||
dispatch("select", exact)
|
dispatch("select", exact);
|
||||||
} else if (event.shiftKey && event.key === "Escape") {
|
|
||||||
dispatch("select", 0)
|
|
||||||
} else if (event.key === "Escape") {
|
|
||||||
dispatch("close")
|
|
||||||
} else if (event.key === "ArrowDown") {
|
} else if (event.key === "ArrowDown") {
|
||||||
const element =
|
const element =
|
||||||
resultList.querySelector("li:focus-within")?.nextSibling ?? resultList.querySelector("li:not(.exact)")
|
resultList.querySelector("li:focus-within")?.nextSibling ??
|
||||||
|
resultList.querySelector("li:not(.exact)");
|
||||||
if (element instanceof HTMLLIElement) {
|
if (element instanceof HTMLLIElement) {
|
||||||
element.querySelector("button")?.focus()
|
element.querySelector("button")?.focus();
|
||||||
}
|
}
|
||||||
} else if (event.key === "ArrowUp") {
|
} else if (event.key === "ArrowUp") {
|
||||||
const element =
|
const element =
|
||||||
resultList.querySelector("li:focus-within")?.previousSibling ??
|
resultList.querySelector("li:focus-within")?.previousSibling ??
|
||||||
resultList.querySelector("li:not(.exact)")
|
resultList.querySelector("li:not(.exact)");
|
||||||
if (element instanceof HTMLLIElement) {
|
if (element instanceof HTMLLIElement) {
|
||||||
element.querySelector("button")?.focus()
|
element.querySelector("button")?.focus();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
searchBox.focus()
|
searchBox.focus();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
let results: number[] = []
|
let results: number[] = [];
|
||||||
let exact: number | undefined = undefined
|
let exact: number | undefined = undefined;
|
||||||
let code: number = Number.NaN
|
let code: number = Number.NaN;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
let searchBox: HTMLInputElement
|
let searchBox: HTMLInputElement;
|
||||||
let resultList: HTMLUListElement
|
let resultList: HTMLUListElement;
|
||||||
|
let filter: Set<number>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={keyboardNavigation} />
|
<svelte:window on:keydown={keyboardNavigation} />
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
||||||
<dialog open on:click|self={() => dispatch("close")}>
|
<dialog open on:click|self={() => dispatch("close")}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
bind:this={searchBox}
|
bind:this={searchBox}
|
||||||
autofocus
|
|
||||||
on:input={search}
|
on:input={search}
|
||||||
on:keypress={event => {
|
on:keypress={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
select(exact)
|
select(exact);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={$LL.actionSearch.PLACEHOLDER()}
|
placeholder={$LL.actionSearch.PLACEHOLDER()}
|
||||||
/>
|
/>
|
||||||
<button on:click={() => select(0)}
|
<button on:click={() => select(0)} use:action={{ shortcut: "shift+esc" }}
|
||||||
><div><span class="icon key-hint">shift</span>+<span class="key-hint">ESC</span></div>
|
>{$LL.actionSearch.DELETE()}</button
|
||||||
{$LL.actionSearch.DELETE()}</button
|
>
|
||||||
|
<button
|
||||||
|
use:action={{ title: $LL.modal.CLOSE(), shortcut: "esc" }}
|
||||||
|
class="icon"
|
||||||
|
on:click={() => dispatch("close")}>close</button
|
||||||
>
|
>
|
||||||
<button title={$LL.modal.CLOSE()} class="icon" on:click={() => dispatch("close")}>close</button>
|
|
||||||
</div>
|
</div>
|
||||||
<aside>
|
<fieldset class="filters">
|
||||||
<h3>{$LL.actionSearch.CURRENT_ACTION()}</h3>
|
<label
|
||||||
<ActionListItem id={currentAction} />
|
>{$LL.actionSearch.filter.ALL()}<input
|
||||||
</aside>
|
checked
|
||||||
|
name="category"
|
||||||
|
type="radio"
|
||||||
|
value={undefined}
|
||||||
|
bind:group={filter}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
{#each KEYMAP_CATEGORIES as category}
|
||||||
|
<label
|
||||||
|
>{category.name}<input
|
||||||
|
name="category"
|
||||||
|
type="radio"
|
||||||
|
value={new Set(Object.keys(category.actions).map(Number))}
|
||||||
|
bind:group={filter}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</fieldset>
|
||||||
|
{#if currentAction !== undefined}
|
||||||
|
<aside>
|
||||||
|
<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}>
|
<ul bind:this={resultList}>
|
||||||
{#if exact !== undefined}
|
{#if exact !== undefined}
|
||||||
<li class="exact">
|
<li class="exact">
|
||||||
<i
|
<i>Exact match</i>
|
||||||
>Exact match <span class="icon key-hint">shift</span>+<span class="icon key-hint"
|
|
||||||
>keyboard_return</span
|
|
||||||
></i
|
|
||||||
>
|
|
||||||
<ActionListItem id={exact} on:click={() => select(exact)} />
|
<ActionListItem id={exact} on:click={() => select(exact)} />
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -116,14 +150,46 @@
|
|||||||
<li>Action code is out of range</li>
|
<li>Action code is out of range</li>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#each results as id (id)}
|
{#if filter !== undefined || results.length > 0}
|
||||||
<li><ActionListItem {id} on:click={() => select(id)} /></li>
|
{@const resultValue =
|
||||||
{/each}
|
results.length === 0
|
||||||
|
? Array.from(KEYMAP_CODES, ([it]) => it)
|
||||||
|
: results}
|
||||||
|
{#each filter ? resultValue.filter( (it) => filter.has(it), ) : resultValue as id (id)}
|
||||||
|
<li><ActionListItem {id} on:click={() => select(id)} /></li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
label {
|
||||||
|
height: unset;
|
||||||
|
padding-block: 2px;
|
||||||
|
padding-inline: 4px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
border: 1px solid currentcolor;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:has(:checked) {
|
||||||
|
color: var(--md-sys-color-on-secondary);
|
||||||
|
background: var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -133,6 +199,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
background: rgba(0 0 0 / 60%);
|
background: rgba(0 0 0 / 60%);
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,10 +221,15 @@
|
|||||||
|
|
||||||
background: var(--md-sys-color-background);
|
background: var(--md-sys-color-background);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
@media (prefers-contrast: more) {
|
||||||
margin-inline: 16px;
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
opacity: 1;
|
||||||
|
color: GrayText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-row {
|
.search-row {
|
||||||
@@ -165,42 +237,11 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-inline: 16px;
|
margin-inline: 16px;
|
||||||
|
|
||||||
> button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
color: currentcolor;
|
|
||||||
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 100%;
|
|
||||||
|
|
||||||
&:not(.icon) {
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transform-origin: top left;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -213,6 +254,10 @@
|
|||||||
|
|
||||||
background: var(--md-sys-color-background);
|
background: var(--md-sys-color-background);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
border: 1px solid CanvasText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="search"] {
|
input[type="search"] {
|
||||||
@@ -227,7 +272,7 @@
|
|||||||
|
|
||||||
background: none;
|
background: none;
|
||||||
border: 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;
|
transition: all 250ms ease;
|
||||||
|
|
||||||
@@ -280,27 +325,9 @@
|
|||||||
background: var(--md-sys-color-primary);
|
background: var(--md-sys-color-primary);
|
||||||
border-radius: 0 0 8px 8px;
|
border-radius: 0 0 8px 8px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.key-hint {
|
@media (forced-colors: active) {
|
||||||
display: inline-flex;
|
background: Mark;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
height: 20px;
|
|
||||||
margin-block: 6px;
|
|
||||||
padding: 2px;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
color: currentcolor;
|
|
||||||
|
|
||||||
border: 1px solid currentcolor;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
&.icon {
|
|
||||||
padding: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
223
src/lib/components/layout/GenericLayout.svelte
Normal file
223
src/lib/components/layout/GenericLayout.svelte
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { compileLayout } from "$lib/serialization/visual-layout";
|
||||||
|
import type {
|
||||||
|
VisualLayout,
|
||||||
|
CompiledLayoutKey,
|
||||||
|
} from "$lib/serialization/visual-layout";
|
||||||
|
import { deviceLayout } from "$lib/serial/connection";
|
||||||
|
import { dev } from "$app/environment";
|
||||||
|
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { VisualLayoutConfig } from "./visual-layout.js";
|
||||||
|
import { changes, ChangeType, layout } from "$lib/undo-redo";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import { expoOut } from "svelte/easing";
|
||||||
|
|
||||||
|
const { scale, margin, strokeWidth, fontSize, iconFontSize } =
|
||||||
|
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||||
|
const activeLayer = getContext<Writable<number>>("active-layer");
|
||||||
|
|
||||||
|
if (dev) {
|
||||||
|
// you have absolutely no idea what a difference this makes for performance
|
||||||
|
console.assert(scale % 1 === 0, "Scale must be an integer");
|
||||||
|
console.assert((scale / 2) % 1 === 0, "Scale must be divisible by 2");
|
||||||
|
console.assert(strokeWidth % 1 === 0, "Stroke must be an integer");
|
||||||
|
console.assert(margin % 1 === 0, "Margin must be an integer");
|
||||||
|
console.assert(fontSize % 1 === 0, "Font size must be an integer");
|
||||||
|
console.assert(iconFontSize % 1 === 0, "Icon font size must be an integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
export let visualLayout: VisualLayout;
|
||||||
|
$: layoutInfo = compileLayout(visualLayout);
|
||||||
|
|
||||||
|
function getCenter(key: CompiledLayoutKey): [x: number, y: number] {
|
||||||
|
return [key.pos[0] + key.size[0] / 2, key.pos[1] + key.size[1] / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDistance(a: CompiledLayoutKey, b: CompiledLayoutKey) {
|
||||||
|
const x1 = a.pos[0] + margin;
|
||||||
|
const y1 = a.pos[1] + margin;
|
||||||
|
const x1b = x1 + a.size[0] - margin;
|
||||||
|
const y1b = y1 + a.size[1] - margin;
|
||||||
|
const x2 = b.pos[0] + margin;
|
||||||
|
const y2 = b.pos[1] + margin;
|
||||||
|
const x2b = x2 + b.size[0] - margin;
|
||||||
|
const y2b = y2 + b.size[1] - margin;
|
||||||
|
|
||||||
|
const left = x2b < x1;
|
||||||
|
const right = x1b < x2;
|
||||||
|
const bottom = y2b < y1;
|
||||||
|
const top = y1b < y2;
|
||||||
|
|
||||||
|
return top && left
|
||||||
|
? Math.sqrt((x1 - x2b) ** 2 + (y1b - y2) ** 2)
|
||||||
|
: left && bottom
|
||||||
|
? Math.sqrt((x1 - x2b) ** 2 + (y1 - y2b) ** 2)
|
||||||
|
: bottom && right
|
||||||
|
? Math.sqrt((x1b - x2) ** 2 + (y1 - y2b) ** 2)
|
||||||
|
: right && top
|
||||||
|
? Math.sqrt((x1b - x2) ** 2 + (y1b - y2) ** 2)
|
||||||
|
: left
|
||||||
|
? x1 - x2b
|
||||||
|
: right
|
||||||
|
? x2 - x1b
|
||||||
|
: bottom
|
||||||
|
? y1 - y2b
|
||||||
|
: top
|
||||||
|
? y2 - y1b
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(event: KeyboardEvent) {
|
||||||
|
if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let wantedAngle: number;
|
||||||
|
const angleThreshold = Math.PI;
|
||||||
|
|
||||||
|
if (event.key === "ArrowUp") wantedAngle = Math.PI;
|
||||||
|
else if (event.key === "ArrowDown") wantedAngle = 0;
|
||||||
|
else if (event.key === "ArrowRight") wantedAngle = Math.PI / 2;
|
||||||
|
else if (event.key === "ArrowLeft") wantedAngle = -Math.PI / 2;
|
||||||
|
else return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
if (!focusKey) (groupParent.firstChild as SVGGElement).focus();
|
||||||
|
const [focusX, focusY] = getCenter(focusKey);
|
||||||
|
|
||||||
|
let bestDistance = Infinity;
|
||||||
|
let bestCandidate = 0;
|
||||||
|
let isOptimalAngle = false;
|
||||||
|
|
||||||
|
for (const [i, key] of layoutInfo.keys.entries()) {
|
||||||
|
if (key === focusKey) continue;
|
||||||
|
const [keyX, keyY] = getCenter(key);
|
||||||
|
const deltaX = keyX - focusX;
|
||||||
|
const deltaY = keyY - focusY;
|
||||||
|
const angle = Math.atan2(deltaX, deltaY);
|
||||||
|
const distance = getDistance(key, focusKey);
|
||||||
|
|
||||||
|
const angleDelta = Math.abs(wantedAngle - angle);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isOptimalAngle
|
||||||
|
? angleDelta > Number.EPSILON
|
||||||
|
: angleDelta >= angleThreshold
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
if (distance > bestDistance) continue;
|
||||||
|
|
||||||
|
bestDistance = distance;
|
||||||
|
bestCandidate = i;
|
||||||
|
isOptimalAngle = angleDelta <= Number.EPSILON;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = groupParent.children.item(bestCandidate);
|
||||||
|
if (node instanceof SVGGElement) {
|
||||||
|
node.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(index: number) {
|
||||||
|
const keyInfo = layoutInfo.keys[index];
|
||||||
|
if (!keyInfo) return;
|
||||||
|
const clickedGroup = groupParent.children.item(index) as SVGGElement;
|
||||||
|
const nextAction = get(layout)[get(activeLayer)]?.[keyInfo.id];
|
||||||
|
const currentAction = get(deviceLayout)[get(activeLayer)]?.[keyInfo.id];
|
||||||
|
if (!nextAction || !currentAction) return;
|
||||||
|
const component = new ActionSelector({
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
currentAction,
|
||||||
|
nextAction: nextAction.isApplied ? undefined : nextAction.action,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dialog = document.querySelector("dialog > div") as HTMLDivElement;
|
||||||
|
const backdrop = document.querySelector("dialog") as HTMLDialogElement;
|
||||||
|
const dialogRect = dialog.getBoundingClientRect();
|
||||||
|
const groupRect = clickedGroup.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
component.$on("close", closed);
|
||||||
|
component.$on("select", ({ detail }) => {
|
||||||
|
changes.update((changes) => {
|
||||||
|
changes.push({
|
||||||
|
type: ChangeType.Layout,
|
||||||
|
id: keyInfo.id,
|
||||||
|
layer: get(activeLayer),
|
||||||
|
action: detail,
|
||||||
|
});
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
|
closed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusKey: CompiledLayoutKey;
|
||||||
|
let groupParent: SVGElement;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={navigate} />
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class="print"
|
||||||
|
viewBox="0 0 {layoutInfo.size[0] * scale} {layoutInfo.size[1] * scale}"
|
||||||
|
bind:this={groupParent}
|
||||||
|
transition:fly={{ y: 48, easing: expoOut }}
|
||||||
|
>
|
||||||
|
{#each layoutInfo.keys as key, i}
|
||||||
|
<KeyboardKey
|
||||||
|
{i}
|
||||||
|
{key}
|
||||||
|
on:focusin={() => (focusKey = key)}
|
||||||
|
on:click={() => edit(i)}
|
||||||
|
on:keypress={({ key }) => {
|
||||||
|
if (key === "Enter") {
|
||||||
|
edit(i);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
svg {
|
||||||
|
overflow: visible;
|
||||||
|
grid-area: "d";
|
||||||
|
width: calc(min(100%, 35cm));
|
||||||
|
max-height: calc(100% - 170px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {layout} from "$lib/serial/connection"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
|
||||||
import {popup} from "$lib/popup"
|
|
||||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
|
||||||
|
|
||||||
export let id: number = 0
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
{#each $layout as layer, i}
|
|
||||||
<tr>
|
|
||||||
<th class="icon">counter_{i + 1}</th>
|
|
||||||
<ActionListItem id={layer[id]} />
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</table>
|
|
||||||
92
src/lib/components/layout/KeyText.svelte
Normal file
92
src/lib/components/layout/KeyText.svelte
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
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";
|
||||||
|
|
||||||
|
const { fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize } =
|
||||||
|
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||||
|
const activeLayer = getContext<Writable<number>>("active-layer");
|
||||||
|
|
||||||
|
export let key: CompiledLayoutKey;
|
||||||
|
export let fontSizeMultiplier = 1;
|
||||||
|
|
||||||
|
export let middle: [number, number];
|
||||||
|
export let pos: [number, number];
|
||||||
|
export let rotate: number;
|
||||||
|
|
||||||
|
export let positions: [[number, number], [number, number], [number, number]];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each positions as position, layer}
|
||||||
|
{@const { action: actionId, isApplied } = $layout[layer]?.[key.id] ?? {
|
||||||
|
action: 0,
|
||||||
|
isApplied: true,
|
||||||
|
}}
|
||||||
|
{@const { code, icon, id, display, title, keyCode, variant } =
|
||||||
|
KEYMAP_CODES.get(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 = [
|
||||||
|
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"}
|
||||||
|
text-anchor="middle"
|
||||||
|
alignment-baseline="central"
|
||||||
|
x={pos[0] + middle[0] + (isApplied ? 0 : fontSize / 3)}
|
||||||
|
y={pos[1] + middle[1]}
|
||||||
|
font-size={fontSizeMultiplier * (hasIcon ? iconFontSize : fontSize)}
|
||||||
|
font-family={hasIcon ? "Material Symbols Rounded" : undefined}
|
||||||
|
opacity={isActive ? 1 : `var(--inactive-opacity, ${inactiveOpacity})`}
|
||||||
|
style:scale={isActive ? 1 : `var(--inactive-scale, ${inactiveScale})`}
|
||||||
|
style:translate={isActive
|
||||||
|
? "0 0 0"
|
||||||
|
: `${direction[0]?.toPrecision(2)}px ${direction[1]?.toPrecision(2)}px 0`}
|
||||||
|
style:rotate="{rotate}deg"
|
||||||
|
use:action={{ title: tooltip }}
|
||||||
|
>
|
||||||
|
{#if code !== 0}
|
||||||
|
{dynamicMapping || icon || display || id || `0x${code.toString(16)}`}
|
||||||
|
{/if}
|
||||||
|
{#if !isApplied}
|
||||||
|
<tspan>•</tspan>
|
||||||
|
{/if}
|
||||||
|
</text>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$focus-transition: 10ms;
|
||||||
|
$transition: 200ms;
|
||||||
|
|
||||||
|
text {
|
||||||
|
will-change: translate, scale;
|
||||||
|
user-select: none;
|
||||||
|
transform-origin: center;
|
||||||
|
transform-box: fill-box;
|
||||||
|
transition:
|
||||||
|
fill #{$focus-transition} ease,
|
||||||
|
opacity #{$transition} ease,
|
||||||
|
translate #{$transition} ease,
|
||||||
|
scale #{$transition} ease;
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
--inactive-opacity: 0.8;
|
||||||
|
--inactive-scale: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text:focus-within {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
133
src/lib/components/layout/KeyboardKey.svelte
Normal file
133
src/lib/components/layout/KeyboardKey.svelte
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { CompiledLayoutKey } from "$lib/serialization/visual-layout";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { VisualLayoutConfig } from "./visual-layout.js";
|
||||||
|
import KeyText from "$lib/components/layout/KeyText.svelte";
|
||||||
|
|
||||||
|
const { scale, margin, strokeWidth } = getContext<VisualLayoutConfig>(
|
||||||
|
"visual-layout-config",
|
||||||
|
);
|
||||||
|
export let i: number;
|
||||||
|
export let key: CompiledLayoutKey;
|
||||||
|
|
||||||
|
$: posX = key.pos[0] * scale;
|
||||||
|
$: posY = key.pos[1] * scale;
|
||||||
|
$: sizeX = key.size[0] * scale;
|
||||||
|
$: sizeY = key.size[1] * scale;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<g
|
||||||
|
class="key-group"
|
||||||
|
on:click
|
||||||
|
on:keypress
|
||||||
|
on:focusin
|
||||||
|
role="button"
|
||||||
|
tabindex={i + 1}
|
||||||
|
>
|
||||||
|
{#if key.shape === "square"}
|
||||||
|
<rect
|
||||||
|
x={posX + margin}
|
||||||
|
y={posY + margin}
|
||||||
|
rx={key.cornerRadius * scale}
|
||||||
|
width={sizeX - margin * 2}
|
||||||
|
height={sizeY - margin * 2}
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
/>
|
||||||
|
<KeyText
|
||||||
|
{key}
|
||||||
|
middle={[sizeX / 2, sizeY / 2]}
|
||||||
|
pos={[posX, posY]}
|
||||||
|
rotate={-key.rotate}
|
||||||
|
positions={[
|
||||||
|
[-1, 1],
|
||||||
|
[-1, -1],
|
||||||
|
[1, -1],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{:else if key.shape === "quarter-circle"}
|
||||||
|
{@const innerMargin = margin / 2}
|
||||||
|
{@const r1 = sizeX / 2 - margin}
|
||||||
|
{@const p1 = r1 - innerMargin}
|
||||||
|
{@const r2 = r1 - sizeY + innerMargin * 2}
|
||||||
|
{@const p2 = r2 - innerMargin}
|
||||||
|
{@const multiplier = 1.25}
|
||||||
|
|
||||||
|
{@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>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$focus-transition: 10ms;
|
||||||
|
$transition: 200ms;
|
||||||
|
|
||||||
|
rect {
|
||||||
|
transform-origin: center;
|
||||||
|
transform-box: fill-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
path,
|
||||||
|
g {
|
||||||
|
transform-origin: top left;
|
||||||
|
transform-box: fill-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
path,
|
||||||
|
rect {
|
||||||
|
fill: var(--md-sys-color-background);
|
||||||
|
fill-opacity: 0;
|
||||||
|
stroke: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: currentcolor;
|
||||||
|
fill-opacity: 0;
|
||||||
|
stroke-opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
g:hover {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity #{$transition} ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
g:focus-within {
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
> path,
|
||||||
|
> rect {
|
||||||
|
fill: currentcolor;
|
||||||
|
fill-opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,39 +1,70 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {serialPort} from "$lib/serial/connection"
|
import { serialPort } from "$lib/serial/connection";
|
||||||
import LayoutCC1 from "$lib/components/layout/LayoutCC1.svelte"
|
import { action } from "$lib/title";
|
||||||
|
import GenericLayout from "$lib/components/layout/GenericLayout.svelte";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import type { VisualLayout } from "$lib/serialization/visual-layout";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
$: device = $serialPort?.device ?? "ONE"
|
$: device = $serialPort?.device;
|
||||||
let activeLayer = 0
|
const activeLayer = getContext<Writable<number>>("active-layer");
|
||||||
|
|
||||||
const layers = [
|
const layers = [
|
||||||
["Numeric Layer", "123", 1],
|
["Numeric Layer", "123", 1],
|
||||||
["Primary Layer", "abc", 0],
|
["Primary Layer", "abc", 0],
|
||||||
["Function Layer", "function", 2],
|
["Function Layer", "function", 2],
|
||||||
] as const
|
] as const;
|
||||||
|
|
||||||
|
const layouts = {
|
||||||
|
ONE: () =>
|
||||||
|
import("$lib/assets/layouts/one.yml").then(
|
||||||
|
(it) => it.default as VisualLayout,
|
||||||
|
),
|
||||||
|
LITE: () =>
|
||||||
|
import("$lib/assets/layouts/lite.yml").then(
|
||||||
|
(it) => it.default as VisualLayout,
|
||||||
|
),
|
||||||
|
X: () =>
|
||||||
|
import("$lib/assets/layouts/generic/103-key.yml").then(
|
||||||
|
(it) => it.default as VisualLayout,
|
||||||
|
),
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div class="container">
|
||||||
<fieldset>
|
{#if device}
|
||||||
{#each layers as [title, icon, value]}
|
{#await layouts[device]() then visualLayout}
|
||||||
<button
|
<fieldset transition:fade>
|
||||||
{title}
|
{#each layers as [title, icon, value]}
|
||||||
class="icon"
|
<button
|
||||||
on:click={() => (activeLayer = value)}
|
class="icon"
|
||||||
class:active={activeLayer === value}
|
use:action={{ title, shortcut: `alt+${value + 1}` }}
|
||||||
>
|
on:click={() => ($activeLayer = value)}
|
||||||
{icon}
|
class:active={$activeLayer === value}
|
||||||
</button>
|
>
|
||||||
{/each}
|
{icon}
|
||||||
</fieldset>
|
</button>
|
||||||
|
{/each}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
{#if device === "ONE"}
|
<GenericLayout {visualLayout} />
|
||||||
<LayoutCC1 bind:activeLayer />
|
{/await}
|
||||||
{:else}
|
|
||||||
<p>Unsupported device ({$serialPort?.device})</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-bottom: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@@ -41,8 +72,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
margin-block-end: -36px;
|
padding: 8px;
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -68,16 +98,23 @@
|
|||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
outline: 8px solid var(--md-sys-color-background);
|
}
|
||||||
|
|
||||||
|
&:first-child,
|
||||||
|
&:last-child {
|
||||||
|
aspect-ratio: unset;
|
||||||
|
height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-inline-end: 16px;
|
margin-inline-end: -8px;
|
||||||
|
padding-inline: 4px 24px;
|
||||||
border-radius: 16px 0 0 16px;
|
border-radius: 16px 0 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
padding-inline-start: 16px;
|
margin-inline-start: -8px;
|
||||||
|
padding-inline: 24px 4px;
|
||||||
border-radius: 0 16px 16px 0;
|
border-radius: 0 16px 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<script>
|
|
||||||
import RingInput from "$lib/components/layout/RingInput.svelte"
|
|
||||||
|
|
||||||
export let activeLayer = 0
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col layout" style="gap: 0">
|
|
||||||
<div class="row" style="gap: 156px">
|
|
||||||
<div class="row">
|
|
||||||
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} />
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} />
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} />
|
|
||||||
</div>
|
|
||||||
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<RingInput {activeLayer} keys={{d: 60, w: 61, n: 62, e: 63, s: 64}} />
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 65, w: 66, n: 67, e: 68, s: 69}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 80, w: 81, n: 82, e: 83, s: 84}} />
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<RingInput {activeLayer} keys={{d: 70, w: 71, n: 72, e: 73, s: 74}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 85, w: 86, n: 87, e: 88, s: 89}} />
|
|
||||||
</div>
|
|
||||||
<RingInput {activeLayer} keys={{d: 75, w: 76, n: 77, e: 78, s: 79}} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row" style="gap: 48px; margin-top: -32px">
|
|
||||||
<RingInput {activeLayer} keys={{d: 10, e: 11, n: 12, w: 13, s: 14}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 55, w: 56, n: 57, e: 58, s: 59}} />
|
|
||||||
</div>
|
|
||||||
<div class="row" style="gap: 160px">
|
|
||||||
<RingInput {activeLayer} keys={{d: 5, e: 6, n: 7, w: 8, s: 9}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
|
||||||
</div>
|
|
||||||
<div class="row" style="gap: 320px; margin-top: -12px">
|
|
||||||
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} />
|
|
||||||
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.row,
|
|
||||||
.col {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {changes, highlightActions, layout} from "$lib/serial/connection"
|
|
||||||
import type {Change} from "$lib/serial/connection"
|
|
||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
|
||||||
import {editableLayout} from "$lib/editable-layout"
|
|
||||||
|
|
||||||
export let activeLayer = 0
|
|
||||||
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
|
|
||||||
|
|
||||||
const virtualLayerMap = [1, 0, 2]
|
|
||||||
const characterOffset = 8
|
|
||||||
|
|
||||||
function offsetDistance(quadrant: number, layer: number, activeLayer: number): number {
|
|
||||||
const layerOffsetIndex = virtualLayerMap[layer] - virtualLayerMap[activeLayer]
|
|
||||||
const layerOffset = quadrant > 2 ? -characterOffset : characterOffset
|
|
||||||
return 25 * quadrant + layerOffsetIndex * layerOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActions(id: number, layout: CharaLayout, changes: Change[]): [KeyInfo, KeyInfo | undefined][] {
|
|
||||||
return Array.from({length: 3}).map((_, i) => {
|
|
||||||
const actionId = layout?.[i][id]
|
|
||||||
const changedId = changes.findLast(it => it?.layout?.[i]?.[id] !== undefined)?.layout![i]![id]
|
|
||||||
if (changedId !== undefined) {
|
|
||||||
return [KEYMAP_CODES[changedId], KEYMAP_CODES[actionId]]
|
|
||||||
} else {
|
|
||||||
return [KEYMAP_CODES[actionId], undefined]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="radial">
|
|
||||||
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
|
||||||
{@const actions = getActions(id, $layout, $changes)}
|
|
||||||
<button
|
|
||||||
use:editableLayout={{activeLayer, id}}
|
|
||||||
class:active={actions.some(([it]) => it && $highlightActions?.includes(it.code))}
|
|
||||||
>
|
|
||||||
{#each actions as [keyInfo, old], layer}
|
|
||||||
{#if keyInfo}
|
|
||||||
<span
|
|
||||||
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
|
||||||
class:icon={!!keyInfo.icon}
|
|
||||||
class:changed={!!old}
|
|
||||||
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
|
||||||
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use "sass:math";
|
|
||||||
|
|
||||||
$border-width: 18px;
|
|
||||||
$gap: 6px;
|
|
||||||
$size: 96;
|
|
||||||
$offset: 14;
|
|
||||||
$scale-difference: 0.2;
|
|
||||||
$transition-time: 750ms;
|
|
||||||
|
|
||||||
.radial {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
container: radial / size;
|
|
||||||
|
|
||||||
width: #{$size * 1px};
|
|
||||||
height: #{$size * 1px};
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
$cr: math.div($size, 2) - 2 * $offset;
|
|
||||||
|
|
||||||
will-change: scale, offset-distance;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
scale: 0.9;
|
|
||||||
offset-path: path(
|
|
||||||
"M#{math.div($size, 2)} #{$offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$size - $offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$offset}Z"
|
|
||||||
);
|
|
||||||
offset-rotate: 0deg;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
opacity: 0.2;
|
|
||||||
|
|
||||||
transition:
|
|
||||||
scale $transition-time ease,
|
|
||||||
opacity $transition-time ease,
|
|
||||||
offset-distance $transition-time ease;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
scale: 1;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.changed {
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
|
||||||
background: var(--md-sys-color-secondary-container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
|
|
||||||
width: 100cqw;
|
|
||||||
height: 100cqh;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
|
|
||||||
mask-image: url("$lib/assets/quater-ring.svg");
|
|
||||||
mask-size: 100% 100%;
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(1) {
|
|
||||||
clip-path: polygon(50% 50%, 0 0, 100% 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
clip-path: polygon(50% 50%, 100% 0, 100% 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
clip-path: polygon(50% 50%, 0 100%, 100% 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(4) {
|
|
||||||
clip-path: polygon(50% 50%, 0 0, 0 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
translate: -50% -50%;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
width: 25cqw;
|
|
||||||
height: 25cqh;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
mask-image: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
9
src/lib/components/layout/visual-layout.ts
Normal file
9
src/lib/components/layout/visual-layout.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface VisualLayoutConfig {
|
||||||
|
scale: number;
|
||||||
|
inactiveScale: number;
|
||||||
|
inactiveOpacity: number;
|
||||||
|
strokeWidth: number;
|
||||||
|
margin: number;
|
||||||
|
fontSize: number;
|
||||||
|
iconFontSize: number;
|
||||||
|
}
|
||||||
41
src/lib/dialogs/ConfirmDialog.svelte
Normal file
41
src/lib/dialogs/ConfirmDialog.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import Dialog from "$lib/dialogs/Dialog.svelte";
|
||||||
|
import ActionString from "$lib/components/ActionString.svelte";
|
||||||
|
|
||||||
|
export let title: string;
|
||||||
|
export let message: string | undefined;
|
||||||
|
export let abortTitle: string;
|
||||||
|
export let confirmTitle: string;
|
||||||
|
|
||||||
|
export let actions: number[] = [];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog>
|
||||||
|
<h1>{@html title}</h1>
|
||||||
|
{#if message}
|
||||||
|
<p>{@html message}</p>
|
||||||
|
{/if}
|
||||||
|
<p><ActionString {actions} /></p>
|
||||||
|
<div class="buttons">
|
||||||
|
<button on:click={() => dispatch("abort")}>{abortTitle}</button>
|
||||||
|
<button class="primary" on:click={() => dispatch("confirm")}
|
||||||
|
>{confirmTitle}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
src/lib/dialogs/Dialog.svelte
Normal file
32
src/lib/dialogs/Dialog.svelte
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
modal.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
let modal: HTMLDialogElement;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog bind:this={modal}>
|
||||||
|
<slot />
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
dialog {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 512px;
|
||||||
|
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
border: none;
|
||||||
|
border-radius: 38px;
|
||||||
|
box-shadow: 0 0 48px rgba(0 0 0 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
217
src/lib/dialogs/PickChangesDialog.svelte
Normal file
217
src/lib/dialogs/PickChangesDialog.svelte
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Dialog from "$lib/dialogs/Dialog.svelte";
|
||||||
|
import type {
|
||||||
|
Change,
|
||||||
|
ChordChange,
|
||||||
|
LayoutChange,
|
||||||
|
SettingChange,
|
||||||
|
} from "$lib/undo-redo";
|
||||||
|
import { ChangeType, chords } from "$lib/undo-redo";
|
||||||
|
import ActionString from "$lib/components/ActionString.svelte";
|
||||||
|
import LL from "../../i18n/i18n-svelte";
|
||||||
|
import { KEYMAP_IDS } from "$lib/serial/keymap-codes";
|
||||||
|
|
||||||
|
export let changes: Change[] = [
|
||||||
|
{ type: ChangeType.Layout, layer: 0, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Layout, layer: 1, id: 1, action: 1 },
|
||||||
|
{ type: ChangeType.Setting, id: 0, setting: 2 },
|
||||||
|
{ type: ChangeType.Setting, id: 0, setting: 2 },
|
||||||
|
{ type: ChangeType.Setting, id: 0, setting: 2 },
|
||||||
|
{ type: ChangeType.Setting, id: 0, setting: 2 },
|
||||||
|
{
|
||||||
|
type: ChangeType.Chord,
|
||||||
|
id: [1],
|
||||||
|
actions: [55],
|
||||||
|
phrase: [55, 63, 37, 36],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ChangeType.Chord,
|
||||||
|
id: [
|
||||||
|
KEYMAP_IDS.get("y")!.code,
|
||||||
|
KEYMAP_IDS.get("r")!.code,
|
||||||
|
KEYMAP_IDS.get("t")!.code,
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
KEYMAP_IDS.get("y")!.code,
|
||||||
|
KEYMAP_IDS.get("r")!.code,
|
||||||
|
KEYMAP_IDS.get("t")!.code,
|
||||||
|
],
|
||||||
|
phrase: [55, 63, 37, 36],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ChangeType.Chord,
|
||||||
|
id: [
|
||||||
|
KEYMAP_IDS.get("y")!.code,
|
||||||
|
KEYMAP_IDS.get("r")!.code,
|
||||||
|
KEYMAP_IDS.get("t")!.code,
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
KEYMAP_IDS.get("y")!.code,
|
||||||
|
KEYMAP_IDS.get("r")!.code,
|
||||||
|
KEYMAP_IDS.get("t")!.code,
|
||||||
|
],
|
||||||
|
phrase: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
$: existingChords = new Set($chords.map((it) => JSON.stringify(it.id)));
|
||||||
|
|
||||||
|
$: layoutChanges = Array.from(
|
||||||
|
{ length: 3 },
|
||||||
|
(_, i) =>
|
||||||
|
changes.filter(
|
||||||
|
(it) => it.type === ChangeType.Layout && it.layer === i,
|
||||||
|
) as LayoutChange[],
|
||||||
|
);
|
||||||
|
$: settingChanges = changes.filter(
|
||||||
|
(it) => it.type === ChangeType.Setting,
|
||||||
|
) as SettingChange[];
|
||||||
|
$: chordChanges = {
|
||||||
|
added: changes.filter(
|
||||||
|
(it) =>
|
||||||
|
it.type === ChangeType.Chord &&
|
||||||
|
it.phrase.length > 0 &&
|
||||||
|
!existingChords.has(JSON.stringify(it.id)),
|
||||||
|
) as ChordChange[],
|
||||||
|
changed: changes.filter(
|
||||||
|
(it) =>
|
||||||
|
it.type === ChangeType.Chord &&
|
||||||
|
it.phrase.length > 0 &&
|
||||||
|
existingChords.has(JSON.stringify(it.id)),
|
||||||
|
) as ChordChange[],
|
||||||
|
deleted: changes.filter(
|
||||||
|
(it) => it.type === ChangeType.Chord && it.phrase.length === 0,
|
||||||
|
) as ChordChange[],
|
||||||
|
};
|
||||||
|
$: totalChordChanges = Object.values(chordChanges).reduce(
|
||||||
|
(acc, curr) => acc + curr.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog>
|
||||||
|
<h1>{$LL.changes.TITLE()}</h1>
|
||||||
|
<h2>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox"
|
||||||
|
/>{$LL.changes.ALL_CHANGES()}</label
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<ul>
|
||||||
|
{#if layoutChanges.some((it) => it.length > 0)}
|
||||||
|
<li>
|
||||||
|
<h3>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
{$LL.changes.layout.TITLE(
|
||||||
|
layoutChanges.reduce((acc, curr) => acc + curr.length, 0),
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
{#each layoutChanges as changes, i}
|
||||||
|
{@const layer = i + 1}
|
||||||
|
{#if changes.length > 0}
|
||||||
|
<li>
|
||||||
|
<h4>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
{$LL.changes.layout.LAYER({
|
||||||
|
changes: changes.length,
|
||||||
|
layer,
|
||||||
|
})}
|
||||||
|
</label>
|
||||||
|
</h4>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if settingChanges.length > 0}
|
||||||
|
<li>
|
||||||
|
<h3>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox"
|
||||||
|
/>{$LL.changes.settings.TITLE(settingChanges.length)}</label
|
||||||
|
>
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if totalChordChanges > 0}
|
||||||
|
<li>
|
||||||
|
<h3>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox"
|
||||||
|
/>{$LL.changes.chords.TITLE(totalChordChanges)}</label
|
||||||
|
>
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
{#each Object.entries(chordChanges) as [category, changes]}
|
||||||
|
{#if changes.length > 0}
|
||||||
|
<li>
|
||||||
|
<h4>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" class="checkbox" />
|
||||||
|
{#if category === "added"}
|
||||||
|
{$LL.changes.chords.NEW_CHORDS(changes.length)}
|
||||||
|
{:else if category === "changed"}
|
||||||
|
{$LL.changes.chords.CHANGED_CHORDS(changes.length)}
|
||||||
|
{:else if category === "deleted"}
|
||||||
|
{$LL.changes.chords.DELETED_CHORDS(changes.length)}
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
</h4>
|
||||||
|
<ul>
|
||||||
|
{#each changes as change}
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
<ActionString display="keys" actions={change.actions} />
|
||||||
|
<ActionString actions={change.phrase} />
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-inline-start: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-inline-start: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
src/lib/dialogs/confirm-dialog.ts
Normal file
33
src/lib/dialogs/confirm-dialog.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte";
|
||||||
|
|
||||||
|
export async function askForConfirmation(
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
confirmTitle: string,
|
||||||
|
abortTitle: string,
|
||||||
|
actions: number[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
const dialog = new ConfirmDialog({
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmTitle,
|
||||||
|
abortTitle,
|
||||||
|
actions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolvePromise: (value: boolean) => void;
|
||||||
|
const resultPromise = new Promise<boolean>((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.$on("abort", () => resolvePromise(false));
|
||||||
|
dialog.$on("confirm", () => resolvePromise(true));
|
||||||
|
|
||||||
|
const result = await resultPromise;
|
||||||
|
dialog.$destroy();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import type {Action} from "svelte/action"
|
|
||||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
|
||||||
import {changes, layout} from "$lib/serial/connection"
|
|
||||||
import {get} from "svelte/store"
|
|
||||||
|
|
||||||
export const editableLayout: Action<HTMLButtonElement, {activeLayer: number; id: number}> = (
|
|
||||||
node,
|
|
||||||
{id, activeLayer},
|
|
||||||
) => {
|
|
||||||
let component: ActionSelector | undefined
|
|
||||||
function present() {
|
|
||||||
component?.$destroy()
|
|
||||||
component = new ActionSelector({
|
|
||||||
target: document.body,
|
|
||||||
props: {currentAction: get(layout)[activeLayer][id]},
|
|
||||||
})
|
|
||||||
component.$on("close", () => {
|
|
||||||
component!.$destroy()
|
|
||||||
})
|
|
||||||
component.$on("select", ({detail}) => {
|
|
||||||
changes.update(changes => {
|
|
||||||
changes.push({layout: {[activeLayer]: {[id]: detail}}})
|
|
||||||
return changes
|
|
||||||
})
|
|
||||||
component!.$destroy()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
node.addEventListener("click", present)
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
node.removeEventListener("click", present)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -73,8 +73,9 @@
|
|||||||
font-stretch: 62.5% 100%;
|
font-stretch: 62.5% 100%;
|
||||||
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-ext-wght-normal.woff2")
|
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-ext-wght-normal.woff2")
|
||||||
format("woff2-variations");
|
format("woff2-variations");
|
||||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020,
|
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
|
||||||
U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* noto-sans-mono-latin-wght-normal */
|
/* noto-sans-mono-latin-wght-normal */
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
font-stretch: 62.5% 100%;
|
font-stretch: 62.5% 100%;
|
||||||
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-wght-normal.woff2")
|
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-wght-normal.woff2")
|
||||||
format("woff2-variations");
|
format("woff2-variations");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301,
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
|
U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F,
|
||||||
U+2215, U+FEFF, U+FFFD;
|
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/lib/os-layout.ts
Normal file
27
src/lib/os-layout.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { get, writable } from "svelte/store";
|
||||||
|
|
||||||
|
export const osLayout = writable<Map<string, string>>(new Map());
|
||||||
|
|
||||||
|
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 () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,31 @@
|
|||||||
import tippy from "tippy.js"
|
import tippy from "tippy.js";
|
||||||
import type {Action} from "svelte/action"
|
import type { Action } from "svelte/action";
|
||||||
import type {ComponentType, SvelteComponent} from "svelte"
|
import type { ComponentType, SvelteComponent } from "svelte";
|
||||||
|
|
||||||
export const popup: Action<HTMLButtonElement, ComponentType> = (node, Component) => {
|
export const popup: Action<HTMLButtonElement, ComponentType> = (
|
||||||
let component: SvelteComponent | undefined
|
node,
|
||||||
let target: HTMLElement | undefined
|
Component,
|
||||||
|
) => {
|
||||||
|
let component: SvelteComponent | undefined;
|
||||||
|
let target: HTMLElement | undefined;
|
||||||
const edit = tippy(node, {
|
const edit = tippy(node, {
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "click",
|
trigger: "click",
|
||||||
onShow(instance) {
|
onShow(instance) {
|
||||||
target = instance.popper.querySelector(".tippy-content") as HTMLElement
|
target = instance.popper.querySelector(".tippy-content") as HTMLElement;
|
||||||
target.classList.add("active")
|
target.classList.add("active");
|
||||||
component ??= new Component({target})
|
component ??= new Component({ target });
|
||||||
},
|
},
|
||||||
onHidden() {
|
onHidden() {
|
||||||
component?.$destroy()
|
component?.$destroy();
|
||||||
target?.classList.remove("active")
|
target?.classList.remove("active");
|
||||||
component = undefined
|
component = undefined;
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
edit.destroy()
|
edit.destroy();
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,37 +1,43 @@
|
|||||||
import type {Action} from "svelte/action"
|
import type { Action } from "svelte/action";
|
||||||
import {persistentWritable} from "$lib/storage"
|
import { persistentWritable } from "$lib/storage";
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
backup: boolean
|
backup: boolean;
|
||||||
autoConnect: boolean
|
autoConnect: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const theme = persistentWritable("user-theme", {
|
export const theme = persistentWritable("user-theme", {
|
||||||
color: "#6D81C7",
|
color: "#6D81C7",
|
||||||
mode: "dark" as "light" | "dark" | "auto",
|
mode: "dark" as "light" | "dark" | "auto",
|
||||||
})
|
});
|
||||||
|
|
||||||
export const userPreferences = persistentWritable<UserPreferences>("user-preferences", {
|
export const userPreferences = persistentWritable<UserPreferences>(
|
||||||
backup: false,
|
"user-preferences",
|
||||||
autoConnect: true,
|
{
|
||||||
})
|
backup: false,
|
||||||
|
autoConnect: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const preference: Action<HTMLInputElement, keyof UserPreferences> = (node, key) => {
|
export const preference: Action<HTMLInputElement, keyof UserPreferences> = (
|
||||||
const unsubscribe = userPreferences.subscribe(it => {
|
node,
|
||||||
node.checked = it[key]
|
key,
|
||||||
})
|
) => {
|
||||||
|
const unsubscribe = userPreferences.subscribe((it) => {
|
||||||
|
node.checked = it[key];
|
||||||
|
});
|
||||||
function update() {
|
function update() {
|
||||||
userPreferences.update(value => {
|
userPreferences.update((value) => {
|
||||||
value[key] = node.checked
|
value[key] = node.checked;
|
||||||
return value
|
return value;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
node.addEventListener("input", update)
|
node.addEventListener("input", update);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
unsubscribe()
|
unsubscribe();
|
||||||
node.removeEventListener("input", update)
|
node.removeEventListener("input", update);
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {createEventDispatcher} from "svelte"
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let ports: SerialPort[]
|
export let ports: SerialPort[];
|
||||||
const dispatch = createEventDispatcher<{confirm: SerialPort | undefined}>()
|
const dispatch = createEventDispatcher<{ confirm: SerialPort | undefined }>();
|
||||||
let selected = ports[0].getInfo().name
|
let selected = ports[0]?.getInfo().name;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog>
|
<dialog>
|
||||||
{#each ports as port}
|
{#each ports as port}
|
||||||
{@const info = port.getInfo()}
|
{@const info = port.getInfo()}
|
||||||
<label>{info.product}<input type="radio" name="port" value={info.name} bind:group={selected} /></label>
|
<label
|
||||||
|
>{info.product}<input
|
||||||
|
type="radio"
|
||||||
|
name="port"
|
||||||
|
value={info.name}
|
||||||
|
bind:group={selected}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<button on:click={() => dispatch("confirm", undefined)}>Cancel</button>
|
<button on:click={() => dispatch("confirm", undefined)}>Cancel</button>
|
||||||
@@ -17,7 +24,7 @@
|
|||||||
on:click={() =>
|
on:click={() =>
|
||||||
dispatch(
|
dispatch(
|
||||||
"confirm",
|
"confirm",
|
||||||
ports.find(it => it.getInfo().name === selected),
|
ports.find((it) => it.getInfo().name === selected),
|
||||||
)}>Ok</button
|
)}>Ok</button
|
||||||
>
|
>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {describe, it, expect} from "vitest"
|
import { describe, it, expect } from "vitest";
|
||||||
import {
|
import {
|
||||||
deserializeActions,
|
deserializeActions,
|
||||||
parseChordActions,
|
parseChordActions,
|
||||||
@@ -6,43 +6,55 @@ import {
|
|||||||
serializeActions,
|
serializeActions,
|
||||||
stringifyChordActions,
|
stringifyChordActions,
|
||||||
stringifyPhrase,
|
stringifyPhrase,
|
||||||
} from "./chord"
|
} from "./chord";
|
||||||
|
|
||||||
describe("chords", function () {
|
describe("chords", function () {
|
||||||
describe("actions", function () {
|
describe("actions", function () {
|
||||||
it("should serialize actions", function () {
|
it("should serialize actions", function () {
|
||||||
expect(serializeActions([32, 51]).toString(16)).toEqual(0xcc200000000000000000000000000n.toString(16))
|
expect(serializeActions([32, 51]).toString(16)).toEqual(
|
||||||
})
|
0xcc200000000000000000000000000n.toString(16),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should deserialize actions", function () {
|
it("should deserialize actions", function () {
|
||||||
expect(deserializeActions(0xcc200000000000000000000000000n)).toEqual([32, 51])
|
expect(deserializeActions(0xcc200000000000000000000000000n)).toEqual([
|
||||||
})
|
32, 51,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
for (let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
it(`should serialize back-forth ${i} actions`, function () {
|
it(`should serialize back-forth ${i} actions`, function () {
|
||||||
const actions = Array.from({length: i}).map((_, i) => i + 1)
|
const actions = Array.from({ length: i }).map((_, i) => i + 1);
|
||||||
expect(deserializeActions(serializeActions(actions))).toEqual(actions)
|
expect(deserializeActions(serializeActions(actions))).toEqual(actions);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("phrase", function () {
|
describe("phrase", function () {
|
||||||
it("should stringify", function () {
|
it("should stringify", function () {
|
||||||
expect(stringifyPhrase([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])).toEqual("206872D4651FFF")
|
expect(stringifyPhrase([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])).toEqual(
|
||||||
})
|
"206872D4651FFF",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should parse", function () {
|
it("should parse", function () {
|
||||||
expect(parsePhrase("206872D4651FFF")).toEqual([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])
|
expect(parsePhrase("206872D4651FFF")).toEqual([
|
||||||
})
|
0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff,
|
||||||
})
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("chord actions", function () {
|
describe("chord actions", function () {
|
||||||
it("should stringify", function () {
|
it("should stringify", function () {
|
||||||
expect(stringifyChordActions([32, 51])).toEqual("000CC200000000000000000000000000")
|
expect(stringifyChordActions([32, 51])).toEqual(
|
||||||
})
|
"000CC200000000000000000000000000",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should parse", function () {
|
it("should parse", function () {
|
||||||
expect(parseChordActions("000CC200000000000000000000000000")).toEqual([32, 51])
|
expect(parseChordActions("000CC200000000000000000000000000")).toEqual([
|
||||||
})
|
32, 51,
|
||||||
})
|
]);
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import {compressActions, decompressActions} from "../serialization/actions"
|
import { compressActions, decompressActions } from "../serialization/actions";
|
||||||
|
|
||||||
export interface Chord {
|
export interface Chord {
|
||||||
actions: number[]
|
actions: number[];
|
||||||
phrase: number[]
|
phrase: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePhrase(phrase: string): number[] {
|
export function parsePhrase(phrase: string): number[] {
|
||||||
return decompressActions(
|
return decompressActions(
|
||||||
Uint8Array.from({length: phrase.length / 2}).map((_, i) =>
|
Uint8Array.from({ length: phrase.length / 2 }).map((_, i) =>
|
||||||
Number.parseInt(phrase.slice(i * 2, i * 2 + 2), 16),
|
Number.parseInt(phrase.slice(i * 2, i * 2 + 2), 16),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyPhrase(phrase: number[]): string {
|
export function stringifyPhrase(phrase: number[]): string {
|
||||||
return [...compressActions(phrase)]
|
return [...compressActions(phrase)]
|
||||||
.map(it => it.toString(16).padStart(2, "0"))
|
.map((it) => it.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
.join("")
|
||||||
.toUpperCase()
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseChordActions(actions: string): number[] {
|
export function parseChordActions(actions: string): number[] {
|
||||||
return deserializeActions(BigInt(`0x${actions}`))
|
return deserializeActions(BigInt(`0x${actions}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyChordActions(actions: number[]): string {
|
export function stringifyChordActions(actions: number[]): string {
|
||||||
return serializeActions(actions).toString(16).padStart(32, "0").toUpperCase()
|
return serializeActions(actions).toString(16).padStart(32, "0").toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,25 +34,24 @@ export function stringifyChordActions(actions: number[]): string {
|
|||||||
* Actions are represented as 10-bit codes, for a maximum of 12 actions
|
* Actions are represented as 10-bit codes, for a maximum of 12 actions
|
||||||
*/
|
*/
|
||||||
export function serializeActions(actions: number[]): bigint {
|
export function serializeActions(actions: number[]): bigint {
|
||||||
let native = 0n
|
let native = 0n;
|
||||||
for (let i = 1; i <= actions.length; i++) {
|
for (let i = 1; i <= actions.length; i++) {
|
||||||
native |= BigInt(actions[actions.length - i] & 0x3ff) << BigInt((12 - i) * 10)
|
native |=
|
||||||
|
BigInt(actions[actions.length - i]! & 0x3ff) << BigInt((12 - i) * 10);
|
||||||
}
|
}
|
||||||
return native
|
return native;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see {serializeActions}
|
* @see {serializeActions}
|
||||||
*/
|
*/
|
||||||
export function deserializeActions(native: bigint): number[] {
|
export function deserializeActions(native: bigint): number[] {
|
||||||
const actions = []
|
const actions = [];
|
||||||
for (let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
const action = Number(native & 0x3ffn)
|
const action = Number(native & 0x3ffn);
|
||||||
if (action !== 0) {
|
actions.push(action);
|
||||||
actions.push(action)
|
native >>= 10n;
|
||||||
}
|
|
||||||
native >>= 10n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions
|
return actions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,108 @@
|
|||||||
import {get, writable} from "svelte/store"
|
import { get, writable } from "svelte/store";
|
||||||
import {CharaDevice} from "$lib/serial/device"
|
import { CharaDevice } from "$lib/serial/device";
|
||||||
import type {Chord} from "$lib/serial/chord"
|
import type { Chord } from "$lib/serial/chord";
|
||||||
import type {Writable} from "svelte/store"
|
import type { Writable } from "svelte/store";
|
||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
import type { CharaLayout } from "$lib/serialization/layout";
|
||||||
import {persistentWritable} from "$lib/storage"
|
import { persistentWritable } from "$lib/storage";
|
||||||
import {userPreferences} from "$lib/preferences"
|
import { userPreferences } from "$lib/preferences";
|
||||||
|
import settingInfo from "$lib/assets/settings.yml";
|
||||||
|
|
||||||
export const serialPort = writable<CharaDevice | undefined>()
|
export const serialPort = writable<CharaDevice | undefined>();
|
||||||
|
|
||||||
export interface SerialLogEntry {
|
export interface SerialLogEntry {
|
||||||
type: "input" | "output" | "system"
|
type: "input" | "output" | "system";
|
||||||
value: string
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serialLog = writable<SerialLogEntry[]>([])
|
export const serialLog = writable<SerialLogEntry[]>([]);
|
||||||
|
|
||||||
export const chords = persistentWritable<Chord[]>("chord-library", [], () => get(userPreferences).backup)
|
/**
|
||||||
|
* Chords as read from the device
|
||||||
|
*/
|
||||||
|
export const deviceChords = persistentWritable<Chord[]>(
|
||||||
|
"chord-library",
|
||||||
|
[],
|
||||||
|
() => get(userPreferences).backup,
|
||||||
|
);
|
||||||
|
|
||||||
export const layout = persistentWritable<CharaLayout>(
|
/**
|
||||||
|
* Layout as read from the device
|
||||||
|
*/
|
||||||
|
export const deviceLayout = persistentWritable<CharaLayout>(
|
||||||
"layout",
|
"layout",
|
||||||
[[], [], []],
|
[[], [], []],
|
||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
)
|
);
|
||||||
|
|
||||||
export interface Change {
|
/**
|
||||||
layout?: Record<number, Record<number, number>>
|
* Settings as read from the device
|
||||||
chords?: never
|
*/
|
||||||
settings?: Record<number, number>
|
export const deviceSettings = persistentWritable<number[]>(
|
||||||
|
"device-settings",
|
||||||
|
[],
|
||||||
|
() => get(userPreferences).backup,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const syncStatus: Writable<
|
||||||
|
"done" | "error" | "downloading" | "uploading"
|
||||||
|
> = writable("done");
|
||||||
|
|
||||||
|
export interface ProgressInfo {
|
||||||
|
max: number;
|
||||||
|
current: number;
|
||||||
}
|
}
|
||||||
|
export const syncProgress = writable<ProgressInfo | undefined>(undefined);
|
||||||
export const changes = persistentWritable<Change[]>("changes", [])
|
|
||||||
|
|
||||||
export const settings = writable({})
|
|
||||||
|
|
||||||
export const unsavedChanges = writable(new Map<number, number>())
|
|
||||||
|
|
||||||
export const highlightActions: Writable<number[]> = writable([])
|
|
||||||
|
|
||||||
export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done")
|
|
||||||
|
|
||||||
export async function initSerial(manual = false) {
|
export async function initSerial(manual = false) {
|
||||||
const device = get(serialPort) ?? new CharaDevice()
|
const device = get(serialPort) ?? new CharaDevice();
|
||||||
await device.init(manual)
|
await device.init(manual);
|
||||||
serialPort.set(device)
|
serialPort.set(device);
|
||||||
|
await sync();
|
||||||
|
}
|
||||||
|
|
||||||
syncStatus.set("downloading")
|
export async function sync() {
|
||||||
const parsedLayout: CharaLayout = [[], [], []]
|
const device = get(serialPort);
|
||||||
|
if (!device) return;
|
||||||
|
const chordCount = await device.getChordCount();
|
||||||
|
syncStatus.set("downloading");
|
||||||
|
|
||||||
|
const max =
|
||||||
|
Object.keys(settingInfo["settings"]).length +
|
||||||
|
device.keyCount * 3 +
|
||||||
|
chordCount;
|
||||||
|
let current = 0;
|
||||||
|
syncProgress.set({ max, current });
|
||||||
|
function progressTick() {
|
||||||
|
current++;
|
||||||
|
syncProgress.set({ max, current });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedSettings: number[] = [];
|
||||||
|
for (const key in settingInfo["settings"]) {
|
||||||
|
try {
|
||||||
|
parsedSettings[Number.parseInt(key)] = await device.getSetting(
|
||||||
|
Number.parseInt(key),
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
progressTick();
|
||||||
|
}
|
||||||
|
deviceSettings.set(parsedSettings);
|
||||||
|
|
||||||
|
const parsedLayout: CharaLayout = [[], [], []];
|
||||||
for (let layer = 1; layer <= 3; layer++) {
|
for (let layer = 1; layer <= 3; layer++) {
|
||||||
for (let i = 0; i < 90; i++) {
|
for (let i = 0; i < device.keyCount; i++) {
|
||||||
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i)
|
parsedLayout[layer - 1]![i] = await device.getLayoutKey(layer, i);
|
||||||
|
progressTick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layout.set(parsedLayout)
|
deviceLayout.set(parsedLayout);
|
||||||
|
|
||||||
const chordCount = await device.getChordCount()
|
const chordInfo = [];
|
||||||
const chordInfo = []
|
|
||||||
for (let i = 0; i < chordCount; i++) {
|
for (let i = 0; i < chordCount; i++) {
|
||||||
chordInfo.push(await device.getChord(i))
|
chordInfo.push(await device.getChord(i));
|
||||||
|
progressTick();
|
||||||
}
|
}
|
||||||
chords.set(chordInfo)
|
deviceChords.set(chordInfo);
|
||||||
syncStatus.set("done")
|
syncStatus.set("done");
|
||||||
|
syncProgress.set(undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,195 +1,343 @@
|
|||||||
import {LineBreakTransformer} from "$lib/serial/line-break-transformer"
|
import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
|
||||||
import {serialLog} from "$lib/serial/connection"
|
import { serialLog } from "$lib/serial/connection";
|
||||||
import type {Chord} from "$lib/serial/chord"
|
import type { Chord } from "$lib/serial/chord";
|
||||||
import {parseChordActions, parsePhrase, stringifyChordActions, stringifyPhrase} from "$lib/serial/chord"
|
import { SemVer } from "$lib/serial/sem-ver";
|
||||||
import {browser} from "$app/environment"
|
import {
|
||||||
|
parseChordActions,
|
||||||
|
parsePhrase,
|
||||||
|
stringifyChordActions,
|
||||||
|
stringifyPhrase,
|
||||||
|
} from "$lib/serial/chord";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
export const VENDOR_ID = 0x239a
|
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||||
|
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
||||||
|
["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }],
|
||||||
|
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
|
||||||
|
["X", { usbProductId: 33163, usbVendorId: 12346 }],
|
||||||
|
]);
|
||||||
|
|
||||||
if (browser && navigator.serial === undefined && import.meta.env.TAURI_FAMILY !== undefined) {
|
const KEY_COUNTS = {
|
||||||
await import("./tauri-serial")
|
ONE: 90,
|
||||||
|
LITE: 67,
|
||||||
|
X: 256,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (
|
||||||
|
browser &&
|
||||||
|
navigator.serial === undefined &&
|
||||||
|
import.meta.env.TAURI_FAMILY !== undefined
|
||||||
|
) {
|
||||||
|
await import("./tauri-serial");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getViablePorts(): Promise<SerialPort[]> {
|
export async function getViablePorts(): Promise<SerialPort[]> {
|
||||||
return navigator.serial.getPorts().then(ports => ports.filter(it => it.getInfo().usbVendorId === VENDOR_ID))
|
return navigator.serial.getPorts().then((ports) =>
|
||||||
|
ports.filter((it) => {
|
||||||
|
const { usbProductId, usbVendorId } = it.getInfo();
|
||||||
|
for (const filter of PORT_FILTERS.values()) {
|
||||||
|
if (
|
||||||
|
filter.usbProductId === usbProductId &&
|
||||||
|
filter.usbVendorId === usbVendorId
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LengthArray<T, N extends number, R extends T[] = []> = number extends N
|
||||||
|
? T[]
|
||||||
|
: R["length"] extends N
|
||||||
|
? R
|
||||||
|
: LengthArray<T, N, [T, ...R]>;
|
||||||
|
|
||||||
export async function canAutoConnect() {
|
export async function canAutoConnect() {
|
||||||
return getViablePorts().then(it => it.length === 1)
|
return getViablePorts().then((it) => it.length === 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||||
|
let timer: number;
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<T>((_, reject) => {
|
||||||
|
timer = setTimeout(
|
||||||
|
() => reject(new Error("Timeout")),
|
||||||
|
ms,
|
||||||
|
) as unknown as number;
|
||||||
|
}),
|
||||||
|
]).finally(() => clearTimeout(timer));
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CharaDevice {
|
export class CharaDevice {
|
||||||
private port!: SerialPort
|
private port!: SerialPort;
|
||||||
private reader!: ReadableStreamDefaultReader<string>
|
private reader!: ReadableStreamDefaultReader<string>;
|
||||||
|
|
||||||
private readonly abortController1 = new AbortController()
|
private readonly abortController1 = new AbortController();
|
||||||
private readonly abortController2 = new AbortController()
|
private readonly abortController2 = new AbortController();
|
||||||
|
|
||||||
private streamClosed!: Promise<void>
|
private streamClosed!: Promise<void>;
|
||||||
|
|
||||||
private lock?: Promise<true>
|
private lock?: Promise<true>;
|
||||||
|
|
||||||
version!: [number, number, number]
|
private readonly suspendDebounce = 100;
|
||||||
company!: "CHARACHORDER"
|
private suspendDebounceId?: number;
|
||||||
device!: "ONE" | "LITE"
|
|
||||||
chipset!: "M0" | "S2"
|
version!: SemVer;
|
||||||
|
company!: "CHARACHORDER";
|
||||||
|
device!: "ONE" | "LITE" | "X";
|
||||||
|
chipset!: "M0" | "S2";
|
||||||
|
keyCount!: 90 | 67 | 256;
|
||||||
|
|
||||||
|
get portInfo() {
|
||||||
|
return this.port.getInfo();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private readonly baudRate = 115200) {}
|
constructor(private readonly baudRate = 115200) {}
|
||||||
|
|
||||||
async init(manual = false) {
|
async init(manual = false) {
|
||||||
const ports = await getViablePorts()
|
try {
|
||||||
this.port =
|
const ports = await getViablePorts();
|
||||||
!manual && ports.length === 1
|
this.port =
|
||||||
? ports[0]
|
!manual && ports.length === 1
|
||||||
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
|
? ports[0]!
|
||||||
await this.port.open({baudRate: this.baudRate})
|
: await navigator.serial.requestPort({
|
||||||
const info = this.port.getInfo()
|
filters: [...PORT_FILTERS.values()],
|
||||||
serialLog.update(it => {
|
});
|
||||||
|
|
||||||
|
await this.port.open({ baudRate: this.baudRate });
|
||||||
|
const info = this.port.getInfo();
|
||||||
|
serialLog.update((it) => {
|
||||||
|
it.push({
|
||||||
|
type: "system",
|
||||||
|
value: `Connected; ID: 0x${info.usbProductId?.toString(
|
||||||
|
16,
|
||||||
|
)}; Vendor: 0x${info.usbVendorId?.toString(16)}`,
|
||||||
|
});
|
||||||
|
return it;
|
||||||
|
});
|
||||||
|
await this.port.close();
|
||||||
|
|
||||||
|
this.version = new SemVer(
|
||||||
|
await this.send(1, "VERSION").then(([version]) => version),
|
||||||
|
);
|
||||||
|
const [company, device, chipset] = await this.send(3, "ID");
|
||||||
|
this.company = company as "CHARACHORDER";
|
||||||
|
this.device = device as "ONE" | "LITE" | "X";
|
||||||
|
this.chipset = chipset as "M0" | "S2";
|
||||||
|
this.keyCount = KEY_COUNTS[this.device];
|
||||||
|
} catch (e) {
|
||||||
|
alert(e);
|
||||||
|
console.error(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async suspend() {
|
||||||
|
await this.reader.cancel();
|
||||||
|
await this.streamClosed.catch(() => {
|
||||||
|
/** noop */
|
||||||
|
});
|
||||||
|
this.reader.releaseLock();
|
||||||
|
await this.port.close();
|
||||||
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "system",
|
type: "system",
|
||||||
value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString(
|
value: "Connection suspended",
|
||||||
16,
|
});
|
||||||
)}`,
|
return it;
|
||||||
})
|
});
|
||||||
return it
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const decoderStream = new TextDecoderStream()
|
private async wake() {
|
||||||
|
await this.port.open({ baudRate: this.baudRate });
|
||||||
|
const decoderStream = new TextDecoderStream();
|
||||||
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
|
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
|
||||||
signal: this.abortController1.signal,
|
signal: this.abortController1.signal,
|
||||||
})
|
});
|
||||||
|
|
||||||
this.reader = decoderStream
|
this.reader = decoderStream
|
||||||
.readable!.pipeThrough(new TransformStream(new LineBreakTransformer()), {
|
.readable!.pipeThrough(new TransformStream(new LineBreakTransformer()), {
|
||||||
signal: this.abortController2.signal,
|
signal: this.abortController2.signal,
|
||||||
})
|
})
|
||||||
.getReader()
|
.getReader();
|
||||||
|
serialLog.update((it) => {
|
||||||
const [version] = await this.send("VERSION")
|
it.push({
|
||||||
this.version = version.split(".").map(Number) as [number, number, number]
|
type: "system",
|
||||||
const [company, device, chipset] = await this.send("ID")
|
value: "Connection resumed",
|
||||||
this.company = company as "CHARACHORDER"
|
});
|
||||||
this.device = device as "ONE" | "LITE"
|
return it;
|
||||||
this.chipset = chipset as "M0" | "S2"
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalRead() {
|
private async internalRead() {
|
||||||
const {value} = await this.reader.read()
|
try {
|
||||||
serialLog.update(it => {
|
const { value } = await timeout(this.reader.read(), 5000);
|
||||||
it.push({
|
serialLog.update((it) => {
|
||||||
type: "output",
|
it.push({
|
||||||
value: value!,
|
type: "output",
|
||||||
})
|
value: value!,
|
||||||
return it
|
});
|
||||||
})
|
return it;
|
||||||
return value!
|
});
|
||||||
|
return value!;
|
||||||
|
} catch (e) {
|
||||||
|
serialLog.update((it) => {
|
||||||
|
it.push({
|
||||||
|
type: "output",
|
||||||
|
value: `${e}`,
|
||||||
|
});
|
||||||
|
return it;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a command to the device
|
* Send a command to the device
|
||||||
*/
|
*/
|
||||||
private async internalSend(...command: string[]) {
|
private async internalSend(...command: string[]) {
|
||||||
const writer = this.port.writable!.getWriter()
|
const writer = this.port.writable!.getWriter();
|
||||||
try {
|
try {
|
||||||
serialLog.update(it => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "input",
|
type: "input",
|
||||||
value: command.join(" "),
|
value: command.join(" "),
|
||||||
})
|
});
|
||||||
return it
|
return it;
|
||||||
})
|
});
|
||||||
await writer.write(new TextEncoder().encode(`${command.join(" ")}\r\n`))
|
await writer.write(new TextEncoder().encode(`${command.join(" ")}\r\n`));
|
||||||
} finally {
|
} finally {
|
||||||
writer.releaseLock()
|
writer.releaseLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async forget() {
|
async forget() {
|
||||||
await this.disconnect()
|
await this.port.forget();
|
||||||
await this.port.forget()
|
|
||||||
}
|
|
||||||
|
|
||||||
async disconnect() {
|
|
||||||
await this.reader.cancel()
|
|
||||||
await this.streamClosed.catch(() => {
|
|
||||||
/** noop */
|
|
||||||
})
|
|
||||||
this.reader.releaseLock()
|
|
||||||
await this.port.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read/write to serial port
|
* Read/write to serial port
|
||||||
*/
|
*/
|
||||||
async runWith<T>(
|
async runWith<T>(
|
||||||
callback: (send: typeof this.internalSend, read: typeof this.internalRead) => T | Promise<T>,
|
callback: (
|
||||||
|
send: typeof this.internalSend,
|
||||||
|
read: typeof this.internalRead,
|
||||||
|
) => T | Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
while (this.lock) {
|
while (this.lock) {
|
||||||
await this.lock
|
await this.lock;
|
||||||
}
|
}
|
||||||
const send = this.internalSend.bind(this)
|
const send = this.internalSend.bind(this);
|
||||||
const read = this.internalRead.bind(this)
|
const read = this.internalRead.bind(this);
|
||||||
const exec = new Promise<T>(async resolve => {
|
let resolveLock: (result: true) => void;
|
||||||
let result!: T
|
this.lock = new Promise<true>((resolve) => {
|
||||||
try {
|
resolveLock = resolve;
|
||||||
result = await callback(send, read)
|
});
|
||||||
} finally {
|
let result!: T;
|
||||||
this.lock = undefined
|
try {
|
||||||
resolve(result)
|
if (this.suspendDebounceId) {
|
||||||
|
clearTimeout(this.suspendDebounceId);
|
||||||
|
} else {
|
||||||
|
await this.wake();
|
||||||
}
|
}
|
||||||
})
|
result = await callback(send, read);
|
||||||
this.lock = exec.then(() => true)
|
} finally {
|
||||||
return exec
|
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;
|
||||||
|
resolveLock!(true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send to serial port
|
* Send to serial port
|
||||||
*/
|
*/
|
||||||
async send(...command: string[]) {
|
async send<T extends number>(
|
||||||
|
expectedLength: T,
|
||||||
|
...command: string[]
|
||||||
|
): Promise<LengthArray<string, T>> {
|
||||||
return this.runWith(async (send, read) => {
|
return this.runWith(async (send, read) => {
|
||||||
await send(...command)
|
await send(...command);
|
||||||
const commandString = command.join(" ").replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
|
const commandString = command
|
||||||
return read().then(it => it.replace(new RegExp(`^${commandString} `), "").split(" "))
|
.join(" ")
|
||||||
})
|
.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||||
|
const readResult = await read();
|
||||||
|
if (readResult === undefined) {
|
||||||
|
console.error("No response");
|
||||||
|
return Array(expectedLength).fill("NO_RESPONSE") as LengthArray<
|
||||||
|
string,
|
||||||
|
T
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
const array = readResult
|
||||||
|
.replace(new RegExp(`^${commandString} `), "")
|
||||||
|
.split(" ");
|
||||||
|
if (array.length < expectedLength) {
|
||||||
|
console.error("Response too short");
|
||||||
|
return array.concat(
|
||||||
|
Array(expectedLength - array.length).fill("TOO_SHORT"),
|
||||||
|
) as LengthArray<string, T>;
|
||||||
|
}
|
||||||
|
return array as LengthArray<string, T>;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChordCount(): Promise<number> {
|
async getChordCount(): Promise<number> {
|
||||||
const [count] = await this.send("CML C0")
|
const [count] = await this.send(1, "CML C0");
|
||||||
return Number.parseInt(count)
|
return Number.parseInt(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a chord by index
|
* Retrieves a chord by index
|
||||||
*/
|
*/
|
||||||
async getChord(index: number | number[]): Promise<Chord> {
|
async getChord(index: number | number[]): Promise<Chord> {
|
||||||
const [actions, phrase] = await this.send(`CML C1 ${index}`)
|
const [actions, phrase] = await this.send(2, `CML C1 ${index}`);
|
||||||
return {
|
return {
|
||||||
actions: parseChordActions(actions),
|
actions: parseChordActions(actions),
|
||||||
phrase: parsePhrase(phrase),
|
phrase: parsePhrase(phrase),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the phrase for a set of actions
|
* Retrieves the phrase for a set of actions
|
||||||
*/
|
*/
|
||||||
async getChordPhrase(actions: number[]): Promise<number[] | undefined> {
|
async getChordPhrase(actions: number[]): Promise<number[] | undefined> {
|
||||||
const [phrase] = await this.send(`CML C2 ${stringifyChordActions(actions)}`)
|
const [phrase] = await this.send(
|
||||||
return phrase === "0" ? undefined : parsePhrase(phrase)
|
1,
|
||||||
|
`CML C2 ${stringifyChordActions(actions)}`,
|
||||||
|
);
|
||||||
|
return phrase === "2" ? undefined : parsePhrase(phrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setChord(chord: Chord) {
|
async setChord(chord: Chord) {
|
||||||
const [status] = await this.send(
|
const [status] = await this.send(
|
||||||
|
1,
|
||||||
"CML",
|
"CML",
|
||||||
"C3",
|
"C3",
|
||||||
stringifyChordActions(chord.actions),
|
stringifyChordActions(chord.actions),
|
||||||
stringifyPhrase(chord.phrase),
|
stringifyPhrase(chord.phrase),
|
||||||
)
|
);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
if (status !== "0") console.error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteChord(chord: Chord) {
|
async deleteChord(chord: Pick<Chord, "actions">) {
|
||||||
const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`)
|
const status = await this.send(
|
||||||
if (status.at(-1) !== "0") throw new Error(`Failed with status ${status}`)
|
1,
|
||||||
|
`CML C4 ${stringifyChordActions(chord.actions)}`,
|
||||||
|
);
|
||||||
|
if (status?.at(-1) !== "2" && status?.at(-1) !== "0")
|
||||||
|
throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,8 +347,8 @@ export class CharaDevice {
|
|||||||
* @param action the assigned action id
|
* @param action the assigned action id
|
||||||
*/
|
*/
|
||||||
async setLayoutKey(layer: number, id: number, action: number) {
|
async setLayoutKey(layer: number, id: number, action: number) {
|
||||||
const [status] = await this.send(`VAR B3 A${layer} ${id} ${action}`)
|
const [status] = await this.send(1, `VAR B4 A${layer} ${id} ${action}`);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,9 +358,9 @@ export class CharaDevice {
|
|||||||
* @returns the assigned action id
|
* @returns the assigned action id
|
||||||
*/
|
*/
|
||||||
async getLayoutKey(layer: number, id: number) {
|
async getLayoutKey(layer: number, id: number) {
|
||||||
const [position, status] = await this.send(`VAR B3 A${layer} ${id}`)
|
const [position, status] = await this.send(2, `VAR B3 A${layer} ${id}`);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
return Number(position)
|
return Number(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,8 +371,8 @@ export class CharaDevice {
|
|||||||
* **This does not need to be called for chords**
|
* **This does not need to be called for chords**
|
||||||
*/
|
*/
|
||||||
async commit() {
|
async commit() {
|
||||||
const [status] = await this.send("VAR B0")
|
const [status] = await this.send(1, "VAR B0");
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,35 +382,49 @@ export class CharaDevice {
|
|||||||
* To permanently store the settings, you *must* call commit.
|
* To permanently store the settings, you *must* call commit.
|
||||||
*/
|
*/
|
||||||
async setSetting(id: number, value: number) {
|
async setSetting(id: number, value: number) {
|
||||||
const [status] = await this.send(`VAR B2 ${id} ${value}`)
|
const [status] = await this.send(
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
1,
|
||||||
|
`VAR B2 ${id.toString(16).toUpperCase()} ${value}`,
|
||||||
|
);
|
||||||
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a setting from the device
|
* Retrieves a setting from the device
|
||||||
*/
|
*/
|
||||||
async getSetting(id: number): Promise<number> {
|
async getSetting(id: number): Promise<number> {
|
||||||
const [value, status] = await this.send(`VAR B1 ${id}`)
|
const [value, status] = await this.send(
|
||||||
if (status !== "0") throw new Error(`Setting "${id}" doesn't exist (Status code ${status})`)
|
2,
|
||||||
return Number(value)
|
`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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reboots the device
|
* Reboots the device
|
||||||
*/
|
*/
|
||||||
async reboot() {
|
async reboot() {
|
||||||
await this.send("RST")
|
await this.send(0, "RST");
|
||||||
await this.disconnect()
|
|
||||||
// TODO: reconnect
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reboots the device to the bootloader
|
* Reboots the device to the bootloader
|
||||||
*/
|
*/
|
||||||
async bootloader() {
|
async bootloader() {
|
||||||
await this.send("RST BOOTLOADER")
|
await this.send(0, "RST BOOTLOADER");
|
||||||
await this.disconnect()
|
}
|
||||||
// TODO: more...
|
|
||||||
|
/**
|
||||||
|
* Resets the device
|
||||||
|
*/
|
||||||
|
async reset(
|
||||||
|
type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC",
|
||||||
|
) {
|
||||||
|
await this.send(0, `RST ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,6 +433,6 @@ export class CharaDevice {
|
|||||||
* This is useful for debugging when there is a suspected heap or stack issue.
|
* This is useful for debugging when there is a suspected heap or stack issue.
|
||||||
*/
|
*/
|
||||||
async getRamBytesAvailable(): Promise<number> {
|
async getRamBytesAvailable(): Promise<number> {
|
||||||
return Number(await this.send("RAM"))
|
return Number(await this.send(1, "RAM").then(([bytes]) => bytes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,38 @@
|
|||||||
import type {ActionInfo, KeymapCategory} from "$lib/assets/keymaps/keymap"
|
import type { ActionInfo, KeymapCategory } from "$lib/assets/keymaps/keymap";
|
||||||
|
|
||||||
export interface KeyInfo extends Partial<ActionInfo> {
|
export interface KeyInfo extends Partial<ActionInfo> {
|
||||||
code: number
|
code: number;
|
||||||
category: KeymapCategory
|
category?: KeymapCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keymaps = (await Promise.all(
|
export const KEYMAP_CATEGORIES = (await Promise.all(
|
||||||
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(async load =>
|
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(
|
||||||
load().then(it => (it as any).default),
|
async (load) => load().then((it) => (it as any).default),
|
||||||
),
|
),
|
||||||
)) as KeymapCategory[]
|
)) as KeymapCategory[];
|
||||||
|
|
||||||
export const KEYMAP_CODES: Record<number, KeyInfo> = Object.fromEntries(
|
export const KEYMAP_CODES = new Map<number, KeyInfo>(
|
||||||
keymaps.flatMap(category =>
|
KEYMAP_CATEGORIES.flatMap((category) =>
|
||||||
Object.entries(category.actions).map(([code, action]) => [
|
Object.entries(category.actions).map(([code, action]) => [
|
||||||
Number(code),
|
Number(code),
|
||||||
{...action, code: Number(code), category},
|
{ ...action, code: Number(code), category },
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
|
export const KEYMAP_KEYCODES = new Map<string, number>(
|
||||||
|
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 = new Map<string, KeyInfo>(
|
||||||
|
KEYMAP_CATEGORIES.flatMap((category) =>
|
||||||
|
Object.entries(category.actions).map(
|
||||||
|
([code, action]) =>
|
||||||
|
[action.id!, { ...action, code: Number(code), category }] as const,
|
||||||
|
),
|
||||||
|
).filter(([id]) => id !== undefined),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
export class LineBreakTransformer {
|
export class LineBreakTransformer {
|
||||||
private chunks = ""
|
private chunks = "";
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
transform(chunk: string, controller: TransformStreamDefaultController) {
|
transform(chunk: string, controller: TransformStreamDefaultController) {
|
||||||
this.chunks += chunk
|
this.chunks += chunk;
|
||||||
const lines = this.chunks.split("\r\n")
|
const lines = this.chunks.split("\r\n");
|
||||||
this.chunks = lines.pop()!
|
this.chunks = lines.pop()!;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
controller.enqueue(line)
|
controller.enqueue(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
flush(controller: TransformStreamDefaultController) {
|
flush(controller: TransformStreamDefaultController) {
|
||||||
controller.enqueue(this.chunks)
|
controller.enqueue(this.chunks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/lib/serial/sem-ver.ts
Normal file
32
src/lib/serial/sem-ver.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export class SemVer {
|
||||||
|
major = 0;
|
||||||
|
minor = 0;
|
||||||
|
patch = 0;
|
||||||
|
preRelease?: string;
|
||||||
|
meta?: string;
|
||||||
|
|
||||||
|
constructor(versionString: string) {
|
||||||
|
const result =
|
||||||
|
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+))?$/.exec(
|
||||||
|
versionString,
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
console.error("Invalid version string:", versionString);
|
||||||
|
} else {
|
||||||
|
const [, major, minor, patch, preRelease, meta] = result;
|
||||||
|
this.major = Number.parseInt(major ?? "NaN");
|
||||||
|
this.minor = Number.parseInt(minor ?? "NaN");
|
||||||
|
this.patch = Number.parseInt(patch ?? "NaN");
|
||||||
|
if (preRelease) this.preRelease = preRelease;
|
||||||
|
if (meta) this.meta = meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return (
|
||||||
|
`${this.major}.${this.minor}.${this.patch}` +
|
||||||
|
(this.preRelease ? `-${this.preRelease}` : "") +
|
||||||
|
(this.meta ? `+${this.meta}` : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,42 +2,53 @@
|
|||||||
* Compress JSON.stringify with gzip
|
* Compress JSON.stringify with gzip
|
||||||
*/
|
*/
|
||||||
export async function stringifyCompressed<T>(chords: T): Promise<Blob> {
|
export async function stringifyCompressed<T>(chords: T): Promise<Blob> {
|
||||||
const stream = new Blob([JSON.stringify(chords)]).stream().pipeThrough(new CompressionStream("gzip"))
|
const stream = new Blob([JSON.stringify(chords)])
|
||||||
return await new Response(stream).blob()
|
.stream()
|
||||||
|
.pipeThrough(new CompressionStream("gzip"));
|
||||||
|
return await new Response(stream).blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decompress JSON.parse with gzip
|
* Decompress JSON.parse with gzip
|
||||||
*/
|
*/
|
||||||
export async function parseCompressed<T>(blob: Blob): Promise<T> {
|
export async function parseCompressed<T>(blob: Blob): Promise<T> {
|
||||||
const stream = blob.stream().pipeThrough(new DecompressionStream("deflate"))
|
const stream = blob.stream().pipeThrough(new DecompressionStream("deflate"));
|
||||||
return await new Response(stream).json()
|
return await new Response(stream).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share JS object as url query param
|
* Share JS object as url query param
|
||||||
*/
|
*/
|
||||||
export async function getSharableUrl(name: string, data: any, baseHref = window.location.href): Promise<URL> {
|
export async function getSharableUrl(
|
||||||
return new Promise(async resolve => {
|
name: string,
|
||||||
const reader = new FileReader()
|
data: any,
|
||||||
|
baseHref = window.location.href,
|
||||||
|
): Promise<URL> {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const reader = new FileReader();
|
||||||
reader.onloadend = function () {
|
reader.onloadend = function () {
|
||||||
const base64String = (reader.result as string).replace(/^data:application\/octet-stream;base64,/, "")
|
const base64String = (reader.result as string).replace(
|
||||||
const url = new URL(baseHref)
|
/^data:application\/octet-stream;base64,/,
|
||||||
url.searchParams.set(name, base64String)
|
"",
|
||||||
resolve(url)
|
);
|
||||||
}
|
const url = new URL(baseHref);
|
||||||
reader.readAsDataURL(await stringifyCompressed(data))
|
url.searchParams.set(name, base64String);
|
||||||
})
|
resolve(url);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(await stringifyCompressed(data));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseSharableUrl<T>(
|
export async function parseSharableUrl<T>(
|
||||||
name: string,
|
name: string,
|
||||||
url: string = window.location.href,
|
url: string = window.location.href,
|
||||||
): Promise<T | undefined> {
|
): Promise<T | undefined> {
|
||||||
const searchParams = new URL(url).searchParams
|
const searchParams = new URL(url).searchParams;
|
||||||
if (!searchParams.has(name)) return
|
if (!searchParams.has(name)) return;
|
||||||
|
|
||||||
return await fetch(`data:application/octet-stream;base64,${searchParams.get(name)}`)
|
return await fetch(
|
||||||
.then(it => it.blob())
|
`data:application/octet-stream;base64,${searchParams.get(name)}`,
|
||||||
.then(it => parseCompressed(it))
|
)
|
||||||
|
.then((it) => it.blob())
|
||||||
|
.then((it) => parseCompressed(it));
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/lib/serial/tauri-serial-extension.d.ts
vendored
8
src/lib/serial/tauri-serial-extension.d.ts
vendored
@@ -1,8 +1,8 @@
|
|||||||
/// <references types="@types/w3c-web-serial" />
|
/// <references types="@types/w3c-web-serial" />
|
||||||
|
|
||||||
interface SerialPortInfo {
|
interface SerialPortInfo {
|
||||||
name?: string
|
name?: string;
|
||||||
serialNumber?: string
|
serialNumber?: string;
|
||||||
manufacturer?: string
|
manufacturer?: string;
|
||||||
product?: string
|
product?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,77 @@
|
|||||||
import {invoke} from "@tauri-apps/api"
|
import { invoke } from "@tauri-apps/api";
|
||||||
import TauriSerialDialog from "$lib/serial/TauriSerialDialog.svelte"
|
import TauriSerialDialog from "$lib/serial/TauriSerialDialog.svelte";
|
||||||
|
|
||||||
export type TauriSerialPort = Pick<
|
export type TauriSerialPort = Pick<
|
||||||
SerialPort,
|
SerialPort,
|
||||||
"getInfo" | "open" | "close" | "readable" | "writable" | "forget"
|
"getInfo" | "open" | "close" | "readable" | "writable" | "forget"
|
||||||
>
|
>;
|
||||||
|
|
||||||
function NativeSerialPort(info: SerialPortInfo): TauriSerialPort {
|
function NativeSerialPort(info: SerialPortInfo): TauriSerialPort {
|
||||||
return {
|
return {
|
||||||
getInfo() {
|
getInfo() {
|
||||||
return info
|
return info;
|
||||||
},
|
},
|
||||||
async open({baudRate}: SerialOptions) {
|
async open({ baudRate }: SerialOptions) {
|
||||||
await invoke("plugin:serial|open", {path: info.name, baudRate})
|
await invoke("plugin:serial|open", { path: info.name, baudRate });
|
||||||
},
|
},
|
||||||
async close() {
|
async close() {
|
||||||
await invoke("plugin:serial|close", {path: info.name})
|
await invoke("plugin:serial|close", { path: info.name });
|
||||||
},
|
},
|
||||||
async forget() {
|
async forget() {
|
||||||
// noop
|
// noop
|
||||||
},
|
},
|
||||||
readable: new ReadableStream({
|
readable: new ReadableStream({
|
||||||
async pull(controller) {
|
async pull(controller) {
|
||||||
const result = await invoke<number[]>("plugin:serial|read", {path: info.name})
|
const result = await invoke<number[]>("plugin:serial|read", {
|
||||||
controller.enqueue(new Uint8Array(result))
|
path: info.name,
|
||||||
|
});
|
||||||
|
controller.enqueue(new Uint8Array(result));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
writable: new WritableStream({
|
writable: new WritableStream({
|
||||||
async write(chunk) {
|
async write(chunk) {
|
||||||
await invoke("plugin:serial|write", {path: info.name, chunk: Array.from(chunk)})
|
await invoke("plugin:serial|write", {
|
||||||
|
path: info.name,
|
||||||
|
chunk: Array.from(chunk),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error polyfill
|
// @ts-expect-error polyfill
|
||||||
// noinspection JSConstantReassignment
|
|
||||||
navigator.serial = {
|
navigator.serial = {
|
||||||
async getPorts(): Promise<SerialPort[]> {
|
async getPorts(): Promise<SerialPort[]> {
|
||||||
return invoke<any[]>("plugin:serial|get_serial_ports").then(ports =>
|
return invoke<any[]>("plugin:serial|get_serial_ports").then((ports) =>
|
||||||
ports.map(NativeSerialPort),
|
ports.map(NativeSerialPort),
|
||||||
) as Promise<SerialPort[]>
|
) as Promise<SerialPort[]>;
|
||||||
},
|
},
|
||||||
async requestPort(options?: SerialPortRequestOptions): Promise<SerialPort> {
|
async requestPort(options?: SerialPortRequestOptions): Promise<SerialPort> {
|
||||||
const ports = await navigator.serial.getPorts().then(ports =>
|
const ports = await navigator.serial.getPorts().then((ports) =>
|
||||||
options?.filters !== undefined
|
options?.filters !== undefined
|
||||||
? ports.filter(port =>
|
? ports.filter((port) =>
|
||||||
options.filters!.some(({usbVendorId, usbProductId}) => {
|
options.filters!.some(({ usbVendorId, usbProductId }) => {
|
||||||
const info = port.getInfo()
|
const info = port.getInfo();
|
||||||
return (
|
return (
|
||||||
(usbVendorId === undefined || info.usbVendorId === usbVendorId) &&
|
(usbVendorId === undefined ||
|
||||||
(usbProductId === undefined || info.usbProductId === usbProductId)
|
info.usbVendorId === usbVendorId) &&
|
||||||
)
|
(usbProductId === undefined ||
|
||||||
|
info.usbProductId === usbProductId)
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: ports,
|
: ports,
|
||||||
)
|
);
|
||||||
|
|
||||||
const dialog = new TauriSerialDialog({target: document.body, props: {ports}})
|
const dialog = new TauriSerialDialog({
|
||||||
const port = await new Promise<SerialPort>(resolve => dialog.$on("confirm", resolve))
|
target: document.body,
|
||||||
dialog.$destroy()
|
props: { ports },
|
||||||
return port
|
});
|
||||||
|
const port = await new Promise<SerialPort>((resolve) =>
|
||||||
|
// @ts-expect-error polyfill
|
||||||
|
dialog.$on("confirm", resolve),
|
||||||
|
);
|
||||||
|
dialog.$destroy();
|
||||||
|
return port;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {describe, it, expect} from "vitest"
|
import { describe, it, expect } from "vitest";
|
||||||
import {compressActions, decompressActions} from "./actions"
|
import { compressActions, decompressActions } from "./actions";
|
||||||
|
|
||||||
describe("layout", function () {
|
describe("layout", function () {
|
||||||
const actions = [1023, 255, 256, 42, 32, 532, 8000]
|
const actions = [1023, 255, 256, 42, 32, 532, 8000];
|
||||||
|
|
||||||
describe("compression", function () {
|
describe("compression", function () {
|
||||||
it("should compress back and forth arrays divisible by 4", function () {
|
it("should compress back and forth arrays divisible by 4", function () {
|
||||||
expect(decompressActions(compressActions(actions))).toEqual(actions)
|
expect(decompressActions(compressActions(actions))).toEqual(actions);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
* Action codes <32 are invalid.
|
* Action codes <32 are invalid.
|
||||||
*/
|
*/
|
||||||
export function compressActions(actions: number[]): Uint8Array {
|
export function compressActions(actions: number[]): Uint8Array {
|
||||||
const buffer = new Uint8Array(actions.length * 2)
|
const buffer = new Uint8Array(actions.length * 2);
|
||||||
let i = 0
|
let i = 0;
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
if (action > 0xff) {
|
if (action > 0xff) {
|
||||||
buffer[i++] = action >>> 8
|
buffer[i++] = action >>> 8;
|
||||||
}
|
}
|
||||||
buffer[i++] = action & 0xff
|
buffer[i++] = action & 0xff;
|
||||||
}
|
}
|
||||||
return buffer.slice(0, i)
|
return buffer.slice(0, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,13 +21,13 @@ export function compressActions(actions: number[]): Uint8Array {
|
|||||||
* @see {compressActions}
|
* @see {compressActions}
|
||||||
*/
|
*/
|
||||||
export function decompressActions(raw: Uint8Array): number[] {
|
export function decompressActions(raw: Uint8Array): number[] {
|
||||||
const actions: number[] = []
|
const actions: number[] = [];
|
||||||
for (let i = 0; i < raw.length; i++) {
|
for (let i = 0; i < raw.length; i++) {
|
||||||
let action = raw[i]
|
let action = raw[i]!;
|
||||||
if (action < 32) {
|
if (action > 0 && action < 32 && i + 1 < raw.length) {
|
||||||
action = (action << 8) | raw[++i]
|
action = (action << 8) | raw[++i]!;
|
||||||
}
|
}
|
||||||
actions.push(action)
|
actions.push(action);
|
||||||
}
|
}
|
||||||
return actions
|
return actions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import {describe, it, expect} from "vitest"
|
import { describe, it, expect } from "vitest";
|
||||||
import {fromBase64, toBase64} from "./base64"
|
import { fromBase64, toBase64 } from "./base64";
|
||||||
|
|
||||||
describe("base64", function () {
|
describe("base64", function () {
|
||||||
const data = new Uint8Array([24, 235, 22, 67, 84, 73, 23, 77, 21])
|
const data = new Uint8Array([24, 235, 22, 67, 84, 73, 23, 77, 21]);
|
||||||
|
|
||||||
it("should convert back-forth", async function () {
|
it("should convert back-forth", async function () {
|
||||||
expect(await fromBase64(await toBase64(new Blob([data]))).then(it => it.arrayBuffer())).toEqual(
|
expect(
|
||||||
data.buffer,
|
await fromBase64(await toBase64(new Blob([data]))).then((it) =>
|
||||||
)
|
it.arrayBuffer(),
|
||||||
})
|
),
|
||||||
})
|
).toEqual(data.buffer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* meaning some chars are swapped for compatibility
|
* meaning some chars are swapped for compatibility
|
||||||
*/
|
*/
|
||||||
export async function toBase64(blob: Blob): Promise<string> {
|
export async function toBase64(blob: Blob): Promise<string> {
|
||||||
return new Promise(async resolve => {
|
return new Promise(async (resolve) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.onloadend = function () {
|
reader.onloadend = function () {
|
||||||
resolve(
|
resolve(
|
||||||
`${(reader.result as string)
|
`${(reader.result as string)
|
||||||
@@ -14,17 +14,20 @@ export async function toBase64(blob: Blob): Promise<string> {
|
|||||||
.replaceAll("+", ".")
|
.replaceAll("+", ".")
|
||||||
.replaceAll("/", "_")
|
.replaceAll("/", "_")
|
||||||
.replaceAll("=", "-")}`,
|
.replaceAll("=", "-")}`,
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fromBase64(base64: string): Promise<Blob> {
|
export async function fromBase64(
|
||||||
|
base64: string,
|
||||||
|
fetch = window.fetch,
|
||||||
|
): Promise<Blob> {
|
||||||
return fetch(
|
return fetch(
|
||||||
`data:application/octet-stream;base64,${base64
|
`data:application/octet-stream;base64,${base64
|
||||||
.replaceAll(".", "+")
|
.replaceAll(".", "+")
|
||||||
.replaceAll("_", "/")
|
.replaceAll("_", "/")
|
||||||
.replaceAll("-", "=")}`,
|
.replaceAll("-", "=")}`,
|
||||||
).then(it => it.blob())
|
).then((it) => it.blob());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
600, 47, 45, 515, 297, 601, 119, 562, 103, 122, 602, 107, 118, 109, 99, 603, 114, 298, 32, 101, 604, 105,
|
600, 47, 45, 515, 297, 601, 119, 562, 103, 122, 602, 107, 118, 109, 99, 603,
|
||||||
127, 46, 111, 605, 39, 512, 44, 117, 552, 513, 514, 550, 540, 607, 335, 338, 336, 337, 608, 565, 568, 566,
|
114, 298, 32, 101, 604, 105, 127, 46, 111, 605, 39, 512, 44, 117, 552, 513,
|
||||||
567, 609, 563, 63, 519, 297, 610, 98, 120, 536, 113, 611, 102, 112, 104, 100, 612, 97, 296, 544, 116, 613,
|
514, 550, 540, 607, 335, 338, 336, 337, 608, 565, 568, 566, 567, 609, 563,
|
||||||
108, 299, 106, 110, 614, 121, 516, 59, 115, 553, 517, 518, 551, 542, 616, 336, 338, 335, 337, 617, 566,
|
63, 519, 297, 610, 98, 120, 536, 113, 611, 102, 112, 104, 100, 612, 97, 296,
|
||||||
568, 565, 567
|
544, 116, 613, 108, 299, 106, 110, 614, 121, 516, 59, 115, 553, 517, 518,
|
||||||
|
551, 542, 616, 336, 338, 335, 337, 617, 566, 568, 565, 567
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0, 92, 45, 515, 297, 0, 119, 562, 91, 93, 0, 55, 56, 57, 48, 0, 49, 298, 51, 50, 0, 52, 127, 54, 53, 0,
|
0, 92, 45, 515, 297, 0, 119, 562, 91, 93, 0, 55, 56, 57, 48, 0, 49, 298, 51,
|
||||||
96, 512, 61, 124, 0, 513, 514, 550, 540, 0, 569, 572, 570, 571, 0, 565, 568, 566, 567, 0, 563, 63, 519,
|
50, 0, 52, 127, 54, 53, 0, 96, 512, 61, 124, 0, 513, 514, 550, 540, 0, 569,
|
||||||
297, 0, 98, 120, 91, 93, 0, 55, 56, 57, 48, 0, 49, 296, 51, 50, 0, 52, 299, 54, 53, 0, 61, 516, 59, 115,
|
572, 570, 571, 0, 565, 568, 566, 567, 0, 563, 63, 519, 297, 0, 98, 120, 91,
|
||||||
0, 517, 518, 551, 542, 0, 570, 572, 569, 571, 0, 566, 568, 565, 567
|
93, 0, 55, 56, 57, 48, 0, 49, 296, 51, 50, 0, 52, 299, 54, 53, 0, 61, 516,
|
||||||
|
59, 115, 0, 517, 518, 551, 542, 0, 570, 572, 569, 571, 0, 566, 568, 565, 567
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0, 47, 45, 515, 297, 0, 119, 324, 325, 122, 0, 320, 321, 322, 323, 0, 314, 298, 316, 315, 0, 317, 127,
|
0, 47, 45, 515, 297, 0, 119, 324, 325, 122, 0, 320, 321, 322, 323, 0, 314,
|
||||||
319, 318, 0, 39, 512, 44, 117, 552, 513, 514, 0, 540, 0, 335, 338, 336, 337, 0, 569, 572, 570, 571, 0,
|
298, 316, 315, 0, 317, 127, 319, 318, 0, 39, 512, 44, 117, 552, 513, 514, 0,
|
||||||
563, 63, 519, 297, 0, 98, 324, 325, 113, 0, 320, 321, 322, 323, 0, 314, 296, 316, 315, 0, 317, 299, 319,
|
540, 0, 335, 338, 336, 337, 0, 569, 572, 570, 571, 0, 563, 63, 519, 297, 0,
|
||||||
318, 0, 121, 516, 59, 115, 553, 517, 518, 0, 542, 0, 336, 338, 335, 337, 0, 570, 572, 569, 571
|
98, 324, 325, 113, 0, 320, 321, 322, 323, 0, 314, 296, 316, 315, 0, 317,
|
||||||
|
299, 319, 318, 0, 121, 516, 59, 115, 553, 517, 518, 0, 542, 0, 336, 338,
|
||||||
|
335, 337, 0, 570, 572, 569, 571
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,37 +1,49 @@
|
|||||||
import {compressActions, decompressActions} from "./actions"
|
import { compressActions, decompressActions } from "./actions";
|
||||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
import { fromBase64, toBase64 } from "$lib/serialization/base64";
|
||||||
|
|
||||||
export interface NewCharaLayout {
|
export interface NewCharaLayout {
|
||||||
charaLayoutVersion: 1
|
charaLayoutVersion: 1;
|
||||||
device: "one" | "lite" | string
|
device: "one" | "lite" | string;
|
||||||
/**
|
/**
|
||||||
* Layers A1-A3, with numeric action codes on each
|
* Layers A1-A3, with numeric action codes on each
|
||||||
*/
|
*/
|
||||||
layers: [number[], number[], number[]]
|
layers: [number[], number[], number[]];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharaLayout = [number[], number[], number[]]
|
export type CharaLayout = [number[], number[], number[]];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a layout into a micro package
|
* Serialize a layout into a micro package
|
||||||
*/
|
*/
|
||||||
export async function serializeLayout(layout: CharaLayout): Promise<Blob> {
|
export async function serializeLayout(layout: CharaLayout): Promise<Blob> {
|
||||||
const items = compressActions(layout.flat())
|
const items = compressActions(layout.flat());
|
||||||
const stream = new Blob([items]).stream().pipeThrough(new CompressionStream("deflate"))
|
const stream = new Blob([items])
|
||||||
return new Response(stream).blob()
|
.stream()
|
||||||
|
.pipeThrough(new CompressionStream("deflate"));
|
||||||
|
return new Response(stream).blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deserializeLayout(layout: Blob): Promise<CharaLayout> {
|
export async function deserializeLayout(layout: Blob): Promise<CharaLayout> {
|
||||||
const stream = layout.stream().pipeThrough(new DecompressionStream("deflate"))
|
const stream = layout
|
||||||
const raw = await new Response(stream).arrayBuffer()
|
.stream()
|
||||||
const actions = decompressActions(new Uint8Array(raw))
|
.pipeThrough(new DecompressionStream("deflate"));
|
||||||
return [actions.slice(0, 90), actions.slice(90, 180), actions.slice(180, 270)]
|
const raw = await new Response(stream).arrayBuffer();
|
||||||
|
const actions = decompressActions(new Uint8Array(raw));
|
||||||
|
return [
|
||||||
|
actions.slice(0, 90),
|
||||||
|
actions.slice(90, 180),
|
||||||
|
actions.slice(180, 270),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function layoutAsUrlComponent(layout: CharaLayout): Promise<string> {
|
export async function layoutAsUrlComponent(
|
||||||
return serializeLayout(layout).then(toBase64)
|
layout: CharaLayout,
|
||||||
|
): Promise<string> {
|
||||||
|
return serializeLayout(layout).then(toBase64);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function layoutFromUrlComponent(base64: string): Promise<CharaLayout> {
|
export async function layoutFromUrlComponent(
|
||||||
return fromBase64(base64).then(deserializeLayout)
|
base64: string,
|
||||||
|
): Promise<CharaLayout> {
|
||||||
|
return fromBase64(base64).then(deserializeLayout);
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/lib/serialization/visual-layout.ts
Normal file
111
src/lib/serialization/visual-layout.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
export interface VisualLayout {
|
||||||
|
name: string;
|
||||||
|
col: VisualLayoutRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Positionable {
|
||||||
|
offset: [number, number];
|
||||||
|
rotate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VisualLayoutRow extends Positionable {
|
||||||
|
row: Array<VisualLayoutKey | VisualLayoutSwitch>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VisualLayoutKey extends Positionable {
|
||||||
|
key: number;
|
||||||
|
size?: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VisualLayoutSwitch extends Positionable {
|
||||||
|
switch: {
|
||||||
|
n: number;
|
||||||
|
e: number;
|
||||||
|
w: number;
|
||||||
|
s: number;
|
||||||
|
d: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompiledLayout {
|
||||||
|
name: string;
|
||||||
|
size: [number, number];
|
||||||
|
keys: CompiledLayoutKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompiledLayoutKey {
|
||||||
|
id: number;
|
||||||
|
shape: "quarter-circle" | "square";
|
||||||
|
cornerRadius: number;
|
||||||
|
size: [number, number];
|
||||||
|
pos: [number, number];
|
||||||
|
rotate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compileLayout(layout: VisualLayout): CompiledLayout {
|
||||||
|
const compiled: CompiledLayout = {
|
||||||
|
name: layout.name,
|
||||||
|
size: [0, 0],
|
||||||
|
keys: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let y = 0;
|
||||||
|
for (const { row, offset } of layout.col) {
|
||||||
|
let x = offset?.[0] ?? 0;
|
||||||
|
y += offset?.[1] ?? 0;
|
||||||
|
let maxHeight = 0;
|
||||||
|
for (const info of row) {
|
||||||
|
const [ox, oy] = info.offset || [0, 0];
|
||||||
|
const rotate = info.rotate || 0;
|
||||||
|
if ("key" in info) {
|
||||||
|
const [width, height] = info.size ?? [1, 1];
|
||||||
|
|
||||||
|
compiled.keys.push({
|
||||||
|
id: info.key,
|
||||||
|
shape: "square",
|
||||||
|
size: [width, height],
|
||||||
|
pos: [x + ox, y + oy],
|
||||||
|
cornerRadius: 0.1,
|
||||||
|
rotate,
|
||||||
|
});
|
||||||
|
|
||||||
|
x += width + ox;
|
||||||
|
maxHeight = Math.max(maxHeight, height + oy);
|
||||||
|
} else if ("switch" in info) {
|
||||||
|
const cx = x + ox + 1;
|
||||||
|
const cy = y + oy + 1;
|
||||||
|
for (const [i, id] of [
|
||||||
|
info.switch.s,
|
||||||
|
info.switch.w,
|
||||||
|
info.switch.n,
|
||||||
|
info.switch.e,
|
||||||
|
].entries()) {
|
||||||
|
compiled.keys.push({
|
||||||
|
id,
|
||||||
|
shape: "quarter-circle",
|
||||||
|
cornerRadius: 0,
|
||||||
|
size: [2, 0.6],
|
||||||
|
pos: [cx, cy],
|
||||||
|
rotate: 90 * i + 45,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
compiled.keys.push({
|
||||||
|
id: info.switch.d,
|
||||||
|
shape: "square",
|
||||||
|
cornerRadius: 0.5,
|
||||||
|
size: [0.8, 0.8],
|
||||||
|
pos: [x + 0.6 + ox, y + 0.6 + oy],
|
||||||
|
rotate: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
x += 2 + ox;
|
||||||
|
maxHeight = Math.max(maxHeight, 2 + oy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y += maxHeight;
|
||||||
|
compiled.size[0] = Math.max(compiled.size[0], x);
|
||||||
|
}
|
||||||
|
compiled.size[1] = y;
|
||||||
|
|
||||||
|
return compiled;
|
||||||
|
}
|
||||||
@@ -1,58 +1,83 @@
|
|||||||
import type {Action} from "svelte/action"
|
import type { Action } from "svelte/action";
|
||||||
import {serialPort, unsavedChanges} from "$lib/serial/connection"
|
import { changes, ChangeType, settings } from "$lib/undo-redo";
|
||||||
import {get} from "svelte/store"
|
|
||||||
|
|
||||||
export const setting: Action<HTMLInputElement, {id: number; inverse?: number; scale?: number}> = function (
|
export const setting: Action<
|
||||||
node: HTMLInputElement,
|
HTMLInputElement | HTMLSelectElement,
|
||||||
{id, inverse, scale},
|
{ id: number; inverse?: number; scale?: number }
|
||||||
|
> = function (
|
||||||
|
node: HTMLInputElement | HTMLSelectElement,
|
||||||
|
{ id, inverse, scale },
|
||||||
) {
|
) {
|
||||||
node.setAttribute("disabled", "")
|
node.setAttribute("disabled", "");
|
||||||
const type = node.getAttribute("type") as "number" | "checkbox"
|
const type = node.getAttribute("type") as "number" | "checkbox" | "range";
|
||||||
|
const isNumeric =
|
||||||
|
type === "number" || type === "range" || node instanceof HTMLSelectElement;
|
||||||
|
const min = node.hasAttribute("min")
|
||||||
|
? Number(node.getAttribute("min"))
|
||||||
|
: undefined;
|
||||||
|
const max = node.hasAttribute("max")
|
||||||
|
? Number(node.getAttribute("max"))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const unsubscribe = serialPort.subscribe(async port => {
|
const unsubscribe = settings.subscribe(async (settings) => {
|
||||||
if (port) {
|
if (id in settings) {
|
||||||
if (type === "number") {
|
const { value, isApplied } = settings[id]!;
|
||||||
const value = Number(await port.getSetting(id).then(it => it.toString()))
|
if (isNumeric) {
|
||||||
node.value = (
|
node.value = (
|
||||||
inverse !== undefined ? inverse / value : scale !== undefined ? scale * value : value
|
inverse !== undefined
|
||||||
).toString()
|
? inverse / value
|
||||||
|
: scale !== undefined
|
||||||
|
? scale * value
|
||||||
|
: value
|
||||||
|
).toString();
|
||||||
} else {
|
} else {
|
||||||
node.checked = await port.getSetting(id).then(it => it !== 0)
|
node.checked = value !== 0;
|
||||||
}
|
}
|
||||||
node.removeAttribute("disabled")
|
if (isApplied) {
|
||||||
} else {
|
node.classList.remove("pending-changes");
|
||||||
node.setAttribute("disabled", "")
|
} else {
|
||||||
}
|
node.classList.add("pending-changes");
|
||||||
})
|
|
||||||
|
|
||||||
async function listener(event: Event) {
|
|
||||||
const currentValue = await get(serialPort)!.getSetting(id)
|
|
||||||
let value = 0
|
|
||||||
if (type === "number") {
|
|
||||||
value = Number((event as InputEvent).data)
|
|
||||||
if (Number.isNaN(value)) return
|
|
||||||
value = inverse !== undefined ? inverse / value : scale !== undefined ? value / scale : value
|
|
||||||
} else {
|
|
||||||
value = node.checked ? 1 : 0
|
|
||||||
}
|
|
||||||
await get(serialPort)!.setSetting(id, value)
|
|
||||||
|
|
||||||
const originalValue = get(unsavedChanges).get(id)
|
|
||||||
unsavedChanges.update(it => {
|
|
||||||
if (originalValue === value) {
|
|
||||||
it.delete(id)
|
|
||||||
} else if (!it.has(id)) {
|
|
||||||
it.set(id, currentValue)
|
|
||||||
}
|
}
|
||||||
return it
|
node.removeAttribute("disabled");
|
||||||
})
|
} else {
|
||||||
|
node.setAttribute("disabled", "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function listener() {
|
||||||
|
let value: number;
|
||||||
|
if (isNumeric) {
|
||||||
|
value = Number(node.value);
|
||||||
|
if (Number.isNaN(value)) return;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.update((changes) => {
|
||||||
|
changes.push({
|
||||||
|
type: ChangeType.Setting,
|
||||||
|
id: id,
|
||||||
|
setting: value,
|
||||||
|
});
|
||||||
|
return changes;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
node.addEventListener("input", listener)
|
|
||||||
|
node.addEventListener("change", listener);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
node.removeEventListener("input", listener)
|
node.removeEventListener("change", listener);
|
||||||
unsubscribe()
|
unsubscribe();
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import type {Action} from "svelte/action"
|
import type { Action } from "svelte/action";
|
||||||
import {readonly, writable} from "svelte/store"
|
import { readonly, writable } from "svelte/store";
|
||||||
|
|
||||||
const setCanShare = writable(false)
|
const setCanShare = writable(false);
|
||||||
export const canShare = readonly(setCanShare)
|
export const canShare = readonly(setCanShare);
|
||||||
|
|
||||||
let shareCallback: ((event: Event) => void) | undefined
|
let shareCallback: ((event: Event) => void) | undefined;
|
||||||
export function triggerShare(event: Event) {
|
export function triggerShare(event: Event) {
|
||||||
shareCallback?.(event)
|
shareCallback?.(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const share: Action<Window, (event: Event) => void> = (node, callback: (event: Event) => void) => {
|
export const share: Action<Window, (event: Event) => void> = (
|
||||||
setCanShare.set(true)
|
_node,
|
||||||
shareCallback = callback
|
callback: (event: Event) => void,
|
||||||
|
) => {
|
||||||
|
setCanShare.set(true);
|
||||||
|
shareCallback = callback;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
setCanShare.set(false)
|
setCanShare.set(false);
|
||||||
shareCallback = undefined
|
shareCallback = undefined;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
64
src/lib/share/action-array.spec.ts
Normal file
64
src/lib/share/action-array.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { deserializeActionArray, serializeActionArray } from "./action-array";
|
||||||
|
|
||||||
|
describe("action array", () => {
|
||||||
|
it("should work with number arrays", () => {
|
||||||
|
expect(
|
||||||
|
deserializeActionArray(serializeActionArray([62, 256, 1235])),
|
||||||
|
).toEqual([62, 256, 1235]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with nested arrays", () => {
|
||||||
|
expect(deserializeActionArray(serializeActionArray([[], [[]]]))).toEqual([
|
||||||
|
[],
|
||||||
|
[[]],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compress back and forth", () => {
|
||||||
|
expect(
|
||||||
|
deserializeActionArray(
|
||||||
|
serializeActionArray([
|
||||||
|
[43, 746, 634],
|
||||||
|
[34, 63],
|
||||||
|
[332, 34],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
[43, 746, 634],
|
||||||
|
[34, 63],
|
||||||
|
[332, 34],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compress a full layout", () => {
|
||||||
|
const layout = Object.freeze([
|
||||||
|
Object.freeze([
|
||||||
|
0, 0, 0, 0, 0, 53, 119, 45, 103, 122, 52, 107, 118, 109, 99, 51, 114,
|
||||||
|
36, 59, 101, 50, 105, 34, 46, 111, 49, 39, 515, 44, 117, 0, 512, 514,
|
||||||
|
513, 550, 0, 319, 318, 321, 320, 326, 315, 314, 317, 316, 0, 0, 0, 0, 0,
|
||||||
|
54, 98, 120, 536, 113, 55, 102, 112, 104, 100, 56, 97, 296, 544, 116,
|
||||||
|
57, 108, 299, 106, 110, 48, 121, 297, 61, 115, 0, 518, 516, 517, 553, 0,
|
||||||
|
336, 338, 335, 337, 0, 325, 322, 323, 324,
|
||||||
|
]),
|
||||||
|
Object.freeze([
|
||||||
|
0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 53, 0, 47, 52, 0, 51, 298, 0, 50, 0,
|
||||||
|
0, 127, 0, 49, 0, 0, 515, 0, 0, 0, 512, 514, 513, 550, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 0, 536, 0, 0, 54, 0, 92, 55, 0, 56,
|
||||||
|
296, 544, 57, 0, 96, 299, 0, 48, 0, 0, 297, 0, 0, 0, 518, 516, 517, 553,
|
||||||
|
0, 336, 338, 335, 337, 0, 0, 0, 0, 0,
|
||||||
|
]),
|
||||||
|
Object.freeze([
|
||||||
|
0, 0, 0, 0, 0, 0, 64, 95, 43, 0, 0, 126, 38, 63, 40, 0, 35, 298, 36,
|
||||||
|
123, 0, 33, 127, 37, 60, 0, 34, 515, 0, 0, 0, 512, 514, 513, 550, 0,
|
||||||
|
333, 331, 330, 334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 536, 0, 0,
|
||||||
|
94, 58, 124, 41, 0, 42, 296, 544, 125, 0, 126, 299, 0, 62, 0, 0, 297, 0,
|
||||||
|
0, 0, 518, 516, 517, 553, 0, 336, 338, 335, 337, 0, 0, 0, 0, 0,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
deserializeActionArray(serializeActionArray(layout as number[][])),
|
||||||
|
).toEqual(layout);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,52 +1,63 @@
|
|||||||
import {compressActions, decompressActions} from "$lib/serialization/actions"
|
import { compressActions, decompressActions } from "../serialization/actions";
|
||||||
import {CHARA_FILE_TYPES} from "$lib/share/share-url"
|
import { CHARA_FILE_TYPES } from "../share/share-url";
|
||||||
|
|
||||||
export type ActionArray = number[] | ActionArray[]
|
export type ActionArray = number[] | ActionArray[];
|
||||||
export function serializeActionArray(array: ActionArray): Uint8Array {
|
export function serializeActionArray(array: ActionArray): Uint8Array {
|
||||||
let out = new Uint8Array(5)
|
let out = new Uint8Array(5);
|
||||||
const writer = new DataView(out.buffer)
|
const writer = new DataView(out.buffer);
|
||||||
writer.setUint32(0, array.length)
|
writer.setUint32(0, array.length);
|
||||||
|
|
||||||
if (array.length === 0) {
|
if (array.length === 0) {
|
||||||
return out
|
return out;
|
||||||
} else if (typeof array[0] === "number") {
|
} else if (typeof array[0] === "number") {
|
||||||
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number"))
|
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number"));
|
||||||
return concatUint8Arrays(out, compressActions(array as number[]))
|
const compressed = compressActions(array as number[]);
|
||||||
|
writer.setUint32(0, compressed.length);
|
||||||
|
return concatUint8Arrays(out, compressed);
|
||||||
} else if (Array.isArray(array[0])) {
|
} else if (Array.isArray(array[0])) {
|
||||||
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array"))
|
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array"));
|
||||||
return concatUint8Arrays(out, ...(array as ActionArray[]).map(serializeActionArray))
|
return concatUint8Arrays(
|
||||||
|
out,
|
||||||
|
...(array as ActionArray[]).map(serializeActionArray),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Not implemented")
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeActionArray(raw: Uint8Array): ActionArray {
|
export function deserializeActionArray(
|
||||||
const reader = new DataView(raw.buffer)
|
raw: Uint8Array,
|
||||||
const length = reader.getUint32(0)
|
cursor = { pos: 0 },
|
||||||
const type = CHARA_FILE_TYPES[reader.getUint8(4)]
|
): ActionArray {
|
||||||
|
const reader = new DataView(raw.buffer);
|
||||||
|
const length = reader.getUint32(cursor.pos);
|
||||||
|
cursor.pos += 4;
|
||||||
|
const type = CHARA_FILE_TYPES[reader.getUint8(cursor.pos)];
|
||||||
|
cursor.pos++;
|
||||||
|
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
return decompressActions(raw.slice(5, 5 + length))
|
const decompressed = decompressActions(
|
||||||
|
raw.slice(cursor.pos, cursor.pos + length),
|
||||||
|
);
|
||||||
|
cursor.pos += length;
|
||||||
|
return decompressed;
|
||||||
} else if (type === "array") {
|
} else if (type === "array") {
|
||||||
const innerLength = reader.getUint32(5)
|
const out = [];
|
||||||
const out = []
|
|
||||||
let cursor = 5
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
out.push(deserializeActionArray(raw.slice(cursor, cursor + innerLength)))
|
out.push(deserializeActionArray(raw, cursor));
|
||||||
cursor += innerLength
|
|
||||||
}
|
}
|
||||||
return out
|
return out;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Not implemented")
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
|
export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
|
||||||
const out = new Uint8Array(arrays.reduce((a, b) => a + b.length, 0))
|
const out = new Uint8Array(arrays.reduce((a, b) => a + b.length, 0));
|
||||||
let offset = 0
|
let offset = 0;
|
||||||
for (const array of arrays) {
|
for (const array of arrays) {
|
||||||
out.set(array, offset)
|
out.set(array, offset);
|
||||||
offset += array.length
|
offset += array.length;
|
||||||
}
|
}
|
||||||
return out
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
export interface CharaFile<T extends string> {
|
export interface CharaFile<T extends string> {
|
||||||
charaVersion: 1
|
charaVersion: 1;
|
||||||
type: T
|
type: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CharaLayoutFile extends CharaFile<"layout"> {
|
export interface CharaLayoutFile extends CharaFile<"layout"> {
|
||||||
device: "one" | "lite" | string
|
device?: "ONE" | "LITE" | string;
|
||||||
layout: [number[], number[], number[]]
|
layout: [number[], number[], number[]];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CharaChordFile extends CharaFile<"chords"> {
|
export interface CharaChordFile extends CharaFile<"chords"> {
|
||||||
chords: [number[], number[]]
|
chords: [number[], number[]][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharaFiles = CharaLayoutFile | CharaChordFile
|
export interface CharaSettingsFile extends CharaFile<"settings"> {
|
||||||
|
settings: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharaBackupFile extends CharaFile<"backup"> {
|
||||||
|
history: [CharaChordFile, CharaLayoutFile, CharaSettingsFile][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CharaFiles = CharaLayoutFile | CharaChordFile | CharaSettingsFile;
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import type {CharaFile, CharaFiles} from "$lib/share/chara-file"
|
import type { CharaFile, CharaFiles } from "../share/chara-file";
|
||||||
import type {ActionArray} from "$lib/share/action-array"
|
import type { ActionArray } from "../share/action-array";
|
||||||
import {deserializeActionArray, serializeActionArray} from "$lib/share/action-array"
|
import {
|
||||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
deserializeActionArray,
|
||||||
|
serializeActionArray,
|
||||||
|
} from "../share/action-array";
|
||||||
|
import { fromBase64, toBase64 } from "../serialization/base64";
|
||||||
|
|
||||||
type CharaLayoutOrder = {
|
type CharaLayoutOrder = {
|
||||||
[K in CharaFiles["type"]]: Array<
|
[K in CharaFiles["type"]]: Array<
|
||||||
[Exclude<keyof Extract<CharaFiles, {type: K}>, keyof CharaFile<any>>, (typeof CHARA_FILE_TYPES)[number]]
|
[
|
||||||
>
|
Exclude<keyof Extract<CharaFiles, { type: K }>, keyof CharaFile<any>>,
|
||||||
}
|
(typeof CHARA_FILE_TYPES)[number],
|
||||||
|
]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
const keys: CharaLayoutOrder = {
|
const keys: CharaLayoutOrder = {
|
||||||
layout: [
|
layout: [
|
||||||
@@ -15,47 +21,61 @@ const keys: CharaLayoutOrder = {
|
|||||||
["device", "string"],
|
["device", "string"],
|
||||||
],
|
],
|
||||||
chords: [["chords", "array"]],
|
chords: [["chords", "array"]],
|
||||||
}
|
settings: [["settings", "array"]],
|
||||||
|
};
|
||||||
|
|
||||||
export const CHARA_FILE_TYPES = ["unknown", "number", "string", "array"] as const
|
export const CHARA_FILE_TYPES = [
|
||||||
|
"unknown",
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"array",
|
||||||
|
] as const;
|
||||||
|
|
||||||
const sep = "\n"
|
const sep = "\n";
|
||||||
|
|
||||||
export async function charaFileToUriComponent<T extends CharaFiles>(file: T): Promise<string> {
|
export async function charaFileToUriComponent<T extends CharaFiles>(
|
||||||
let url = `${file.type}${sep}${file.charaVersion}`
|
file: T,
|
||||||
|
): Promise<string> {
|
||||||
|
let url = `${file.type}${sep}${file.charaVersion}`;
|
||||||
|
|
||||||
for (const [key, type] of keys[file.type]) {
|
for (const [key, type] of keys[file.type]) {
|
||||||
const value = file[key as keyof T]
|
const value = file[key as keyof T];
|
||||||
url += sep
|
url += sep;
|
||||||
if (type === "string") {
|
if (type === "string") {
|
||||||
url += value as string
|
url += value as string;
|
||||||
} else if (type === "array") {
|
} else if (type === "array") {
|
||||||
const stream = new Blob([serializeActionArray(value as ActionArray)])
|
const stream = new Blob([serializeActionArray(value as ActionArray)])
|
||||||
.stream()
|
.stream()
|
||||||
.pipeThrough(new CompressionStream("deflate"))
|
.pipeThrough(new CompressionStream("deflate"));
|
||||||
url += await toBase64(await new Response(stream).blob())
|
url += await toBase64(await new Response(stream).blob());
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Not implemented")
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return url
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function charaFileFromUriComponent<T extends CharaFiles>(uriComponent: string): Promise<T> {
|
export async function charaFileFromUriComponent<T extends CharaFiles>(
|
||||||
const [fileType, version, ...values] = uriComponent.split(sep)
|
uriComponent: string,
|
||||||
const file: any = {type: fileType, version: Number(version)}
|
fetch = window.fetch,
|
||||||
|
): Promise<T> {
|
||||||
|
const [fileType, version, ...values] = uriComponent.split(sep);
|
||||||
|
const file: any = { type: fileType, charaVersion: Number(version) };
|
||||||
|
|
||||||
for (const [key, type] of keys[fileType as keyof typeof keys]) {
|
for (const [key, type] of keys[fileType as keyof typeof keys]) {
|
||||||
const value = values.pop()!
|
const value = values.shift()!;
|
||||||
if (type === "string") {
|
if (type === "string") {
|
||||||
file[key] = value
|
file[key] = value;
|
||||||
} else if (type === "array") {
|
} else if (type === "array") {
|
||||||
const stream = (await fromBase64(value)).stream().pipeThrough(new DecompressionStream("deflate"))
|
const stream = (await fromBase64(value, fetch))
|
||||||
const actions = new Uint8Array(await new Response(stream).arrayBuffer())
|
.stream()
|
||||||
file[key] = deserializeActionArray(actions)
|
.pipeThrough(new DecompressionStream("deflate"));
|
||||||
|
const actions = new Uint8Array(await new Response(stream).arrayBuffer());
|
||||||
|
console.log(actions);
|
||||||
|
file[key] = deserializeActionArray(actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return file
|
return file;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
import type {Writable} from "svelte/store"
|
import type { Writable } from "svelte/store";
|
||||||
import {writable} from "svelte/store"
|
import { writable } from "svelte/store";
|
||||||
import {browser} from "$app/environment"
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
export function persistentWritable<T>(key: string, value: T, condition?: () => boolean): Writable<T> {
|
export function persistentWritable<T>(
|
||||||
|
key: string,
|
||||||
|
value: T,
|
||||||
|
condition?: () => boolean,
|
||||||
|
): Writable<T> {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const persistedValue = localStorage.getItem(key)
|
const persistedValue = localStorage.getItem(key);
|
||||||
const store = persistedValue !== null ? writable(JSON.parse(persistedValue)) : writable(value)
|
const store =
|
||||||
store.subscribe(value => {
|
persistedValue !== null
|
||||||
if (!condition || condition()) localStorage.setItem(key, JSON.stringify(value))
|
? writable(JSON.parse(persistedValue))
|
||||||
})
|
: writable(value);
|
||||||
|
store.subscribe((value) => {
|
||||||
|
if (!condition || condition())
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
});
|
||||||
|
|
||||||
return store
|
return store;
|
||||||
} else {
|
} else {
|
||||||
return writable(value)
|
return writable(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/lib/style/_kbd.scss
Normal file
35
src/lib/style/_kbd.scss
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
kbd {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 20px;
|
||||||
|
margin-block: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
border: 1px solid currentcolor;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(> kbd) {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> kbd {
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/lib/style/form/_button.scss
Normal file
109
src/lib/style/form/_button.scss
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
label:has(input),
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: max-content;
|
||||||
|
height: 48px;
|
||||||
|
padding-block: 8px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
@media not (forced-colors: active) {
|
||||||
|
color: currentcolor;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
border: 1px solid ButtonBorder;
|
||||||
|
color: ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: 32px;
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
aspect-ratio: 1;
|
||||||
|
padding-block: 0;
|
||||||
|
padding-inline: 0;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
padding: 2px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media not (forced-colors: active) {
|
||||||
|
label:has(input):hover,
|
||||||
|
.button:hover:not(:active),
|
||||||
|
a:hover:not(:active),
|
||||||
|
button:hover:not(:active) {
|
||||||
|
filter: brightness(70%);
|
||||||
|
transition: filter 250ms ease;
|
||||||
|
|
||||||
|
&:has(:checked),
|
||||||
|
&.active {
|
||||||
|
filter: brightness(120%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled,
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
label:has(input) .button,
|
||||||
|
a button {
|
||||||
|
&:hover {
|
||||||
|
color: ActiveText;
|
||||||
|
}
|
||||||
|
&.active,
|
||||||
|
&:active {
|
||||||
|
color: SelectedItemText;
|
||||||
|
background: SelectedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled,
|
||||||
|
:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
@media not (forced-colors: active) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
color: GrayText;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user