mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2025-12-27 21:26:15 +00:00
Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
|||
|
c93246ee8c
|
|||
|
22905c2b56
|
|||
|
074f1da48d
|
|||
|
e7a52221d2
|
|||
|
f03b4d586b
|
|||
|
4cd9ce536d
|
|||
|
a39f57bac1
|
|||
|
bf96c1e29d
|
42
.github/workflows/build.yml
vendored
42
.github/workflows/build.yml
vendored
@@ -2,13 +2,13 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
branches: ["master"]
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 🔨 Build
|
||||
CI:
|
||||
name: 🔨🚀 Build and deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🚚 Checkout
|
||||
@@ -39,26 +39,12 @@ jobs:
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
deploy:
|
||||
name: 🚀 Deploy
|
||||
if: github.event_name == 'push' && contains(github.event.head_commit.message, '[deploy]')
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
environment:
|
||||
name: Website
|
||||
url: https://dotio.theaninova.de
|
||||
steps:
|
||||
- name: 📦 Download build artifacts
|
||||
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 }}
|
||||
- name: Disable jekyll
|
||||
run: touch build/.nojekyll
|
||||
- name: Custom domain
|
||||
run: echo 'manager.charachorder.com' > build/CNAME
|
||||
- run: git config user.name github-actions
|
||||
- run: git config user.email github-actions@github.com
|
||||
- run: git --work-tree build add --all
|
||||
- run: git commit -m "Automatic Deploy action run by github-actions"
|
||||
- run: git push origin HEAD:gh-pages --force
|
||||
|
||||
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: 'publish'
|
||||
name: "publish desktop apps"
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- "desktop-app-v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
with:
|
||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
|
||||
releaseName: 'App v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
releaseName: "App v__VERSION__"
|
||||
releaseBody: "See the assets to download this version and install."
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
.direnv
|
||||
!.env.example
|
||||
venv
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
@@ -6,6 +6,7 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/src-tauri/target
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
|
||||
}
|
||||
6
.prettierrc.cjs
Normal file
6
.prettierrc.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
...require("@theaninova/prettier-config"),
|
||||
plugins: ["prettier-plugin-svelte"],
|
||||
pluginSearchDirs: ["."],
|
||||
overrides: [{files: "*.svelte", options: {parser: "svelte"}}],
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.0/schema/typesafe-i18n.json",
|
||||
"baseLocale": "en",
|
||||
"adapter": "svelte"
|
||||
}
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||
"baseLocale": "en",
|
||||
"adapter": "svelte"
|
||||
}
|
||||
|
||||
37
CONTRIBUTING.md
Normal file
37
CONTRIBUTING.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Contributing
|
||||
|
||||
## UX Principles
|
||||
|
||||
- **Opinionated.** There should never be two ways to do the same thing.
|
||||
- **Intuitive.** If a feature needs a description to explain it,
|
||||
the feature has failed.
|
||||
- **Simple.** No useless buttons that always need to be pressed.
|
||||
|
||||
## UI Design
|
||||
|
||||
The UI design is based on Material 3.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Nix
|
||||
|
||||
[Enable flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes), then start the development shell using
|
||||
|
||||
```shell
|
||||
nix develop
|
||||
```
|
||||
|
||||
You may need to run through some additional setup to get Rust running inside IntelliJ.
|
||||
|
||||
### Other platforms
|
||||
|
||||
- NodeJS >=18.16
|
||||
- Python >=3.10
|
||||
- Rust Stable (For Tauri Development)
|
||||
|
||||
I know, python in JS projects is extremely annoying, unfortunately,
|
||||
it seems to be the only platform that offers a functional
|
||||
way to subset variable woff2 fonts with ligatures.
|
||||
|
||||
In other words, either have python as a development dependency or
|
||||
serve a 3.5MB icons font of which 99.5% is completely unused.
|
||||
48
README.md
48
README.md
@@ -1,40 +1,12 @@
|
||||
# amaCC1ng
|
||||
# CharaChorder Device Manager
|
||||
|
||||

|
||||

|
||||
[](https://dotio.theaninova.de/)
|
||||
The official device manager and configuration tool for CharaChorder devices.
|
||||
|
||||
_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).
|
||||
|
||||
I aim to create a new site that offers an easier, visually pleasing
|
||||
and more complete way to configure and learn CharaChorder devices.
|
||||
|
||||
## Development
|
||||
|
||||
### Nix
|
||||
|
||||
[Enable flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes), then start the development shell using
|
||||
|
||||
```shell
|
||||
nix develop
|
||||
```
|
||||
|
||||
You may need to run through some additional setup to get Rust running inside IntelliJ.
|
||||
|
||||
### Other platforms
|
||||
|
||||
- NodeJS >=18.16
|
||||
- Python >=3.10
|
||||
- Rust Stable (For Tauri Development)
|
||||
|
||||
I know, python in JS projects is extremely annoying, unfortunately,
|
||||
it seems to be the only platform that offers a functional
|
||||
way to subset variable woff2 fonts with ligatures.
|
||||
|
||||
In other words, either have python as a development dependency or
|
||||
serve a 3.5MB icons font of which 99.5% is completely unused.
|
||||
Get the latest desktop release [here](https://github.com/CharaChorder/DeviceManager/releases).
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -57,3 +29,11 @@ To double-check, make sure your private key starts with
|
||||
|
||||
After that, add the `SSH_SERVER`, `SSH_PORT`, `SSH_PRIVATE_KEY` and `SSH_USER`
|
||||
environment secrets to your environment in GitHub.
|
||||
|
||||
## Releases
|
||||
|
||||
Change the version in
|
||||
|
||||
- [package.json](package.json)
|
||||
- [tauri.conf.json](src-tauri/tauri.conf.json)
|
||||
- [Cargo.toml](src-tauri/Cargo.toml)
|
||||
|
||||
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";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
rust-bin = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" "rust-std" "clippy" "rust-analyzer" ];
|
||||
};
|
||||
fontMin = (pkgs.python311.withPackages(ps: with ps; [ brotli fonttools ] ++ (with fonttools.optional-dependencies; [ woff ])));
|
||||
tauriPkgs = nixpkgs.legacyPackages.${system};
|
||||
libraries = with tauriPkgs; [
|
||||
webkitgtk
|
||||
gtk3
|
||||
cairo
|
||||
gdk-pixbuf
|
||||
glib
|
||||
dbus
|
||||
openssl_3
|
||||
librsvg
|
||||
];
|
||||
packages = (with pkgs; [
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
rust-overlay,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
overlays = [(import rust-overlay)];
|
||||
pkgs = import nixpkgs {inherit system overlays;};
|
||||
rust-bin = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = ["rust-src" "rust-std" "clippy" "rust-analyzer"];
|
||||
};
|
||||
fontMin = pkgs.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
|
||||
tauriPkgs = nixpkgs.legacyPackages.${system};
|
||||
libraries = with tauriPkgs; [
|
||||
webkitgtk
|
||||
gtk3
|
||||
cairo
|
||||
gdk-pixbuf
|
||||
glib
|
||||
dbus
|
||||
openssl_3
|
||||
librsvg
|
||||
];
|
||||
packages =
|
||||
(with pkgs; [
|
||||
nodejs_18
|
||||
rust-bin
|
||||
fontMin
|
||||
]) ++ (with tauriPkgs; [
|
||||
])
|
||||
++ (with tauriPkgs; [
|
||||
curl
|
||||
wget
|
||||
pkg-config
|
||||
@@ -39,16 +45,15 @@
|
||||
libsoup
|
||||
webkitgtk
|
||||
librsvg
|
||||
# serial plugin
|
||||
udev
|
||||
# serial plugin
|
||||
udev
|
||||
]);
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = packages;
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
||||
'';
|
||||
};
|
||||
});
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = packages;
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
export interface IconsConfig {
|
||||
codePoints: Record<string, string>
|
||||
inputPath: string
|
||||
outputPath: string
|
||||
icons: string[]
|
||||
}
|
||||
|
||||
const config: IconsConfig = {
|
||||
/** @type {import('./src/tools/icons-config').IconsConfig} */
|
||||
const config = {
|
||||
inputPath:
|
||||
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
||||
outputPath: "src/lib/assets/icons.min.woff2",
|
||||
icons: [
|
||||
"adjust",
|
||||
"add",
|
||||
"piano",
|
||||
"keyboard",
|
||||
"settings",
|
||||
@@ -25,6 +21,7 @@ const config: IconsConfig = {
|
||||
"cable",
|
||||
"person",
|
||||
"sync",
|
||||
"school",
|
||||
"restart_alt",
|
||||
"usb",
|
||||
"usb_off",
|
||||
@@ -59,6 +56,43 @@ const config: IconsConfig = {
|
||||
"translate",
|
||||
"play_arrow",
|
||||
"extension",
|
||||
"upload_file",
|
||||
"commit",
|
||||
"bug_report",
|
||||
"delete",
|
||||
"remove_selection",
|
||||
"bolt",
|
||||
"undo",
|
||||
"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: {
|
||||
speed: "e9e4",
|
||||
@@ -70,6 +104,13 @@ const config: IconsConfig = {
|
||||
counter_3: "f782",
|
||||
ios_share: "e6b8",
|
||||
light_mode: "e518",
|
||||
upload_file: "e9fc",
|
||||
no_sound: "e710",
|
||||
sentiment_extremely_dissatisfied: "f194",
|
||||
download_2: "f523",
|
||||
upload_2: "ff52",
|
||||
stat_minus_2: "e69c",
|
||||
stat_2: "e699",
|
||||
},
|
||||
}
|
||||
|
||||
1425
package-lock.json
generated
1425
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -1,8 +1,16 @@
|
||||
{
|
||||
"name": "amacc1ng",
|
||||
"version": "0.4.1",
|
||||
"name": "charachorder-device-manager",
|
||||
"version": "1.3.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CharaChorder/DeviceManager.git"
|
||||
},
|
||||
"homepage": "https://docs.charachorder.com",
|
||||
"bugs": {
|
||||
"url": "https://github.com/CharaChorder/DeviceManager/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm-run-all --parallel vite typesafe-i18n",
|
||||
"dev:tauri": "tauri dev",
|
||||
@@ -15,61 +23,62 @@
|
||||
"postinstall": "patch-package",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
|
||||
"minify-icons": "node src/tools/minify-icon-font.js",
|
||||
"version": "node src/tools/version.js && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
|
||||
"lint": "prettier --plugin-search-dir . --check .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"typesafe-i18n": "typesafe-i18n"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-javascript": "^6.1.9",
|
||||
"@codemirror/language": "^6.8.0",
|
||||
"@codemirror/commands": "^6.2.5",
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@codemirror/state": "^6.2.1",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.6",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.7",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.16",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.17",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "^1.22.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.3",
|
||||
"@sveltejs/kit": "^1.24.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.5",
|
||||
"@tauri-apps/api": "^1.4.0",
|
||||
"@tauri-apps/cli": "^1.4.0",
|
||||
"@theaninova/prettier-config": "^1.0.0",
|
||||
"@types/dom-view-transitions": "^1.0.1",
|
||||
"@types/flexsearch": "^0.7.3",
|
||||
"@types/w3c-web-serial": "^1.0.3",
|
||||
"@vite-pwa/sveltekit": "^0.2.5",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"@types/w3c-web-usb": "^1.0.10",
|
||||
"@vite-pwa/sveltekit": "^0.2.7",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"codemirror": "^6.0.1",
|
||||
"cypress": "^12.17.3",
|
||||
"cypress": "^13.1.0",
|
||||
"flexsearch": "^0.7.31",
|
||||
"fontkit": "^2.0.2",
|
||||
"glob": "^10.3.3",
|
||||
"glob": "^10.3.4",
|
||||
"hotkeys-js": "^3.12.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-svelte": "^3.0.3",
|
||||
"sass": "^1.64.2",
|
||||
"stylelint": "^15.10.2",
|
||||
"stylelint-config-clean-order": "^5.0.1",
|
||||
"sass": "^1.66.1",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint-config-clean-order": "^5.2.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-prettier-scss": "^1.0.0",
|
||||
"stylelint-config-recommended-scss": "^12.0.0",
|
||||
"stylelint-config-standard-scss": "^10.0.0",
|
||||
"svelte": "^4.1.2",
|
||||
"svelte-check": "^3.4.6",
|
||||
"stylelint-config-recommended-scss": "^13.0.0",
|
||||
"stylelint-config-standard-scss": "^11.0.0",
|
||||
"svelte": "^4.2.0",
|
||||
"svelte-check": "^3.5.1",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"tippy.js": "^6.3.7",
|
||||
"ts-node": "^10.9.1",
|
||||
"typesafe-i18n": "^5.26.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.8",
|
||||
"typesafe-i18n": "^5.26.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-mkcert": "^1.16.0",
|
||||
"vite-plugin-pwa": "^0.16.4",
|
||||
"vitest": "^0.34.1"
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
"vitest": "^0.34.4"
|
||||
},
|
||||
"type": "module",
|
||||
"prettier": "@theaninova/prettier-config"
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -85,7 +85,7 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.4.1"
|
||||
version = "1.3.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
||||
license = "AGPL-3"
|
||||
@@ -18,7 +18,7 @@ tauri-build = { version = "1.4.0", features = [] }
|
||||
serde_json = "1.0"
|
||||
serialport = "4.2.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.4.0", features = ["updater"] }
|
||||
tauri = { version = "1.4.0", features = ["updater", "devtools"] }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
||||
@@ -6,21 +6,14 @@
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": {
|
||||
"productName": "amacc1ng",
|
||||
"version": "0.4.1"
|
||||
},
|
||||
"package": { "productName": "amacc1ng", "version": "1.3.0" },
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false
|
||||
},
|
||||
"allowlist": { "all": false },
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "AGPL-3.0-or-later",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"deb": { "depends": [] },
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
@@ -47,9 +40,7 @@
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"security": { "csp": null },
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" href="%sveltekit.assets%/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
5
src/env.d.ts
vendored
5
src/env.d.ts
vendored
@@ -7,6 +7,11 @@ interface ImportMetaEnv {
|
||||
readonly TAURI_ARCH?: string
|
||||
readonly TAURI_DEBUG?: boolean
|
||||
readonly TAURI_PLATFORM_TYPE?: string
|
||||
|
||||
readonly VITE_HOMEPAGE_URL: string
|
||||
readonly VITE_BUGS_URL: string
|
||||
readonly VITE_DOCS_URL: string
|
||||
readonly VIET_LEARN_URL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -1,14 +1,52 @@
|
||||
import type {Translation} from "../i18n-types"
|
||||
|
||||
const de = {
|
||||
TITLE: "amaCC1ng",
|
||||
TITLE: "CharaChorder Gerätemanager",
|
||||
DESCRIPTION: "Gerätemanager und Konfigurationstool für CharaChorder Geräte.",
|
||||
saveActions: {
|
||||
UNDO: "Rückgängig (<kbd class='icon'>shift</kbd> halten um alle Änderungen rückgängig zu machen)",
|
||||
REDO: "Wiederholen",
|
||||
SAVE: "Speichern",
|
||||
},
|
||||
update: {
|
||||
TITLE: "Gerät aktualisieren",
|
||||
},
|
||||
sync: {
|
||||
TITLE_READ: "Neueste Änderungen werden abgerufen",
|
||||
TITLE_WRITE: "Änderungen werden gespeichert",
|
||||
RELOAD: "Neu laden",
|
||||
},
|
||||
backup: {
|
||||
TITLE: "Sicherungskopie",
|
||||
TITLE: "Verlauf speichern",
|
||||
INDIVIDUAL: "Einzeldateien",
|
||||
DISCLAIMER:
|
||||
"Sicherungskopien verlassen unter keinen Umständen diesen Computer und werden nie mit uns geteilt oder auf Server hochgeladen.",
|
||||
DOWNLOAD: "Kopie Speichern",
|
||||
"Der Verlauf wird als Backup in diesem Browser gespeichert. Der Verlauf bleibt auf diesem Computer.",
|
||||
DOWNLOAD: "Alles herunterladen",
|
||||
RESTORE: "Wiederherstellen",
|
||||
},
|
||||
modal: {
|
||||
CLOSE: "Schließen",
|
||||
},
|
||||
actionSearch: {
|
||||
PLACEHOLDER: "Nach Aktionen suchen",
|
||||
CURRENT_ACTION: "Aktuelle Aktion",
|
||||
NEXT_ACTION: "Aktion nach dem nächsten Speichern",
|
||||
DELETE: "Entfernen",
|
||||
filter: {
|
||||
ALL: "Alle",
|
||||
},
|
||||
LIVE_LAYOUT_INFO: "Diese Aktion wurde auf Basis des Systemtastaturlayouts ermittelt.",
|
||||
SHIFT_WARNING: "Diese Aktion hält <kbd class='icon'>shift</kbd>",
|
||||
ALT_CODE_WARNING: "Dieses Alt-Code Makro funktioniert nur unter Windows",
|
||||
},
|
||||
share: {
|
||||
TITLE: "Teilen",
|
||||
URL_COPIED: "Teilbare URL kopiert!",
|
||||
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
||||
},
|
||||
print: {
|
||||
TITLE: "Drucken",
|
||||
},
|
||||
profile: {
|
||||
TITLE: "Profil",
|
||||
LANGUAGE: "Sprache",
|
||||
@@ -25,10 +63,15 @@ const de = {
|
||||
CONNECT: "Verbinden",
|
||||
DISCONNECT: "Entfernen",
|
||||
TERMINAL: "Konsole",
|
||||
APPLY_SETTINGS: "Änderungen auf das Gerät brennen",
|
||||
NO_DEVICE: "Kein Gerät verbunden",
|
||||
LINUX_PERMISSIONS:
|
||||
"Auf den meisten Linux-basierten Systemen müssen zuerst Berechtigungen angepasst werden.",
|
||||
bootMenu: {
|
||||
TITLE: "Bootmenü",
|
||||
REBOOT: "Neustarten",
|
||||
BOOTLOADER: "Bootloader",
|
||||
POWER_WARNING: "Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.",
|
||||
},
|
||||
},
|
||||
browserWarning: {
|
||||
@@ -41,14 +84,44 @@ const de = {
|
||||
"Auch wenn alle Chromium-basieren Desktop Browser diese Voraussetzung grundsätzlich erfüllen, haben einige Browser ",
|
||||
INFO_BROWSER_INFIX: "wie zum Beispiel Brave",
|
||||
INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.",
|
||||
DOWNLOAD_APP: "Desktop-app herunterladen",
|
||||
DOWNLOAD_APP:
|
||||
"Chrome oder Edge werden offiziell unterstützt, andere Browser könnten aber auch funktionieren.",
|
||||
},
|
||||
changes: {
|
||||
TITLE: "Änderungen importieren",
|
||||
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: {
|
||||
chords: {
|
||||
TITLE: "Akkorde",
|
||||
HOLD_KEYS: "Akkord halten",
|
||||
NEW_CHORD: "Neuer Akkord",
|
||||
DUPLICATE: "Akkord existiert bereits",
|
||||
search: {
|
||||
PLACEHOLDER: "{0} Akkord{{|e}} durchsuchen",
|
||||
NO_RESULTS: "Keine Ergebnisse",
|
||||
},
|
||||
conflict: {
|
||||
TITLE: "Akkordkonflikt",
|
||||
DESCRIPTION:
|
||||
"Der Akkord {0} würde einen bereits existierenden Akkord überschreiben. Wirklich fortfahren?",
|
||||
CONFIRM: "Überschreiben",
|
||||
ABORT: "Überspringen",
|
||||
},
|
||||
TRY_TYPING: "Versuche hier zu tippen",
|
||||
},
|
||||
layout: {
|
||||
TITLE: "Layout",
|
||||
|
||||
@@ -1,13 +1,51 @@
|
||||
import type {BaseTranslation} from "../i18n-types"
|
||||
|
||||
const en = {
|
||||
TITLE: "amaCC1ng",
|
||||
TITLE: "CharaChorder Device Manager",
|
||||
DESCRIPTION: "The device manager and configuration tool for CharaChorder devices.",
|
||||
saveActions: {
|
||||
UNDO: "Undo (hold <kbd class='icon'>shift</kbd> to undo all changes)",
|
||||
REDO: "Redo",
|
||||
SAVE: "Save",
|
||||
},
|
||||
update: {
|
||||
TITLE: "Update your device",
|
||||
},
|
||||
backup: {
|
||||
TITLE: "Local Backup",
|
||||
DISCLAIMER: "Backups remain on your computer and are never shared with us or uploaded to our servers.",
|
||||
DOWNLOAD: "Download Backup",
|
||||
TITLE: "Store History",
|
||||
INDIVIDUAL: "Individual backups",
|
||||
DISCLAIMER: "Your history is stored as a backup in this browser. The history remains on your computer.",
|
||||
DOWNLOAD: "Download Everything",
|
||||
RESTORE: "Restore",
|
||||
},
|
||||
sync: {
|
||||
TITLE_READ: "Reading latest changes",
|
||||
TITLE_WRITE: "Saving changes to device",
|
||||
RELOAD: "Reload",
|
||||
},
|
||||
modal: {
|
||||
CLOSE: "Close",
|
||||
},
|
||||
actionSearch: {
|
||||
PLACEHOLDER: "Search for actions",
|
||||
CURRENT_ACTION: "Current action",
|
||||
NEXT_ACTION: "Action after next save",
|
||||
DELETE: "Remove",
|
||||
filter: {
|
||||
ALL: "All",
|
||||
},
|
||||
LIVE_LAYOUT_INFO: "This output was determined using on your system layout.",
|
||||
SHIFT_WARNING: "This action holds <kbd class='icon'>shift</kbd>",
|
||||
ALT_CODE_WARNING: "This alt-code macro only works on Windows",
|
||||
},
|
||||
share: {
|
||||
TITLE: "Share",
|
||||
URL_COPIED: "Sharable URL copied!",
|
||||
EXTRA_DOWNLOAD: "Download as file",
|
||||
},
|
||||
print: {
|
||||
TITLE: "Print",
|
||||
},
|
||||
profile: {
|
||||
TITLE: "Profile",
|
||||
LANGUAGE: "Language",
|
||||
@@ -24,10 +62,14 @@ const en = {
|
||||
CONNECT: "Connect",
|
||||
DISCONNECT: "Disconnect",
|
||||
TERMINAL: "Terminal",
|
||||
APPLY_SETTINGS: "Flash changes to device",
|
||||
NO_DEVICE: "No device connected",
|
||||
LINUX_PERMISSIONS: "Most Linux based systems need adjusted permissions in order to connect your device.",
|
||||
bootMenu: {
|
||||
TITLE: "Boot Menu",
|
||||
REBOOT: "Reboot",
|
||||
BOOTLOADER: "Bootloader",
|
||||
POWER_WARNING: "To reboot from bootloader you need to physically reconnect your device.",
|
||||
},
|
||||
},
|
||||
browserWarning: {
|
||||
@@ -39,14 +81,43 @@ const en = {
|
||||
"Though all chromium-based desktop browsers fulfill this requirement, some derivations such as Brave ",
|
||||
INFO_BROWSER_INFIX: "have been known to disable the API intentionally",
|
||||
INFO_BROWSER_SUFFIX: ".",
|
||||
DOWNLOAD_APP: "Download the desktop app",
|
||||
DOWNLOAD_APP: "Chrome or Edge are officially supported, but other browsers might work as well.",
|
||||
},
|
||||
changes: {
|
||||
TITLE: "Import changes",
|
||||
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: {
|
||||
chords: {
|
||||
TITLE: "Chords",
|
||||
HOLD_KEYS: "Hold chord",
|
||||
NEW_CHORD: "New chord",
|
||||
DUPLICATE: "Chord already exists",
|
||||
search: {
|
||||
PLACEHOLDER: "Search {0} chord{{|s}}",
|
||||
NO_RESULTS: "No results",
|
||||
},
|
||||
conflict: {
|
||||
TITLE: "Chord conflict",
|
||||
DESCRIPTION:
|
||||
"Your chord {0} conflicts with an existing chord. Are you sure you want to overwrite this chord?",
|
||||
CONFIRM: "Overwrite",
|
||||
ABORT: "Skip",
|
||||
},
|
||||
TRY_TYPING: "Try typing here",
|
||||
},
|
||||
layout: {
|
||||
TITLE: "Layout",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { FormattersInitializer } from 'typesafe-i18n'
|
||||
import type { Locales, Formatters } from './i18n-types'
|
||||
import type {FormattersInitializer} from "typesafe-i18n"
|
||||
import type {Locales, Formatters} from "./i18n-types"
|
||||
|
||||
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
||||
const formatters: Formatters = {
|
||||
// add your formatter functions here
|
||||
}
|
||||
|
||||
const formatters: Formatters = {
|
||||
// add your formatter functions here
|
||||
}
|
||||
|
||||
return formatters
|
||||
return formatters
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type {Action} from "svelte/action"
|
||||
import Index from "flexsearch"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import tippy from "tippy.js"
|
||||
import ActionAutocomplete from "$lib/components/ActionAutocomplete.svelte"
|
||||
import {browser} from "$app/environment"
|
||||
|
||||
const index = browser ? new Index({tokenize: "full"}) : undefined
|
||||
for (const action of Object.values(KEYMAP_CODES)) {
|
||||
index?.add(
|
||||
action.code,
|
||||
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
||||
action.description || ""
|
||||
}`,
|
||||
)
|
||||
}
|
||||
const exact = Object.fromEntries(
|
||||
Object.values(KEYMAP_CODES)
|
||||
.filter(it => !!it.id)
|
||||
.map(it => [it.id, it] as const),
|
||||
)
|
||||
|
||||
export const actionAutocomplete: Action<HTMLInputElement> = node => {
|
||||
if (!browser) return
|
||||
|
||||
let completionComponent: ActionAutocomplete
|
||||
const completionDialog = tippy(node, {
|
||||
interactive: true,
|
||||
placement: "bottom-start",
|
||||
hideOnClick: false,
|
||||
theme: "surface-variant search-completion",
|
||||
arrow: false,
|
||||
trigger: "focus",
|
||||
offset: [0, 0],
|
||||
onCreate(instance) {
|
||||
const target = instance.popper.querySelector(".tippy-content")!
|
||||
completionComponent = new ActionAutocomplete({target, props: {width: node.clientWidth}})
|
||||
},
|
||||
onDestroy() {
|
||||
completionComponent.$destroy()
|
||||
},
|
||||
})
|
||||
|
||||
function input(event: Event) {
|
||||
completionComponent.$set({
|
||||
results: index!.search(node.value),
|
||||
exact: exact[node.value],
|
||||
code: Number(node.value),
|
||||
})
|
||||
}
|
||||
|
||||
node.addEventListener("input", input)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("input", input)
|
||||
},
|
||||
}
|
||||
}
|
||||
141
src/lib/assets/keymaps/ascii-macros.yml
Normal file
141
src/lib/assets/keymaps/ascii-macros.yml
Normal file
@@ -0,0 +1,141 @@
|
||||
name: ASCII Macros
|
||||
description: ASCII Characters that are macros for SHFT + key
|
||||
actions:
|
||||
33:
|
||||
id: "!"
|
||||
title: Exclamation Point
|
||||
34:
|
||||
id: '"'
|
||||
title: Double Quote
|
||||
35:
|
||||
id: "#"
|
||||
title: Hash Symbol
|
||||
36:
|
||||
id: "$"
|
||||
title: Dollar Sign
|
||||
37:
|
||||
id: "%"
|
||||
title: Percent
|
||||
38:
|
||||
id: "&"
|
||||
title: Ampersand
|
||||
40:
|
||||
id: "("
|
||||
title: Opening Parenthesis
|
||||
41:
|
||||
id: ")"
|
||||
title: Closing Parenthesis
|
||||
42:
|
||||
id: "*"
|
||||
title: Asterisk
|
||||
58:
|
||||
id: ":"
|
||||
title: Colon
|
||||
60:
|
||||
id: "<"
|
||||
title: Less Than
|
||||
62:
|
||||
id: ">"
|
||||
title: Greater Than
|
||||
63:
|
||||
id: "?"
|
||||
title: Question Mark
|
||||
64:
|
||||
id: "@"
|
||||
title: At Symbol
|
||||
65:
|
||||
id: "A"
|
||||
title: Uppercase A
|
||||
66:
|
||||
id: "B"
|
||||
title: Uppercase B
|
||||
67:
|
||||
id: "C"
|
||||
title: Uppercase C
|
||||
68:
|
||||
id: "D"
|
||||
title: Uppercase D
|
||||
69:
|
||||
id: "E"
|
||||
title: Uppercase E
|
||||
70:
|
||||
id: "F"
|
||||
title: Uppercase F
|
||||
71:
|
||||
id: "G"
|
||||
title: Uppercase G
|
||||
72:
|
||||
id: "H"
|
||||
title: Uppercase H
|
||||
73:
|
||||
id: "I"
|
||||
title: Uppercase I
|
||||
74:
|
||||
id: "J"
|
||||
title: Uppercase J
|
||||
75:
|
||||
id: "K"
|
||||
title: Uppercase K
|
||||
76:
|
||||
id: "L"
|
||||
title: Uppercase L
|
||||
77:
|
||||
id: "M"
|
||||
title: Uppercase M
|
||||
78:
|
||||
id: "N"
|
||||
title: Uppercase N
|
||||
79:
|
||||
id: "O"
|
||||
title: Uppercase O
|
||||
80:
|
||||
id: "P"
|
||||
title: Uppercase P
|
||||
81:
|
||||
id: "Q"
|
||||
title: Uppercase Q
|
||||
82:
|
||||
id: "R"
|
||||
title: Uppercase R
|
||||
83:
|
||||
id: "S"
|
||||
title: Uppercase S
|
||||
84:
|
||||
id: "T"
|
||||
title: Uppercase T
|
||||
85:
|
||||
id: "U"
|
||||
title: Uppercase U
|
||||
86:
|
||||
id: "V"
|
||||
title: Uppercase V
|
||||
87:
|
||||
id: "W"
|
||||
title: Uppercase W
|
||||
88:
|
||||
id: "X"
|
||||
title: Uppercase X
|
||||
89:
|
||||
id: "Y"
|
||||
title: Uppercase Y
|
||||
90:
|
||||
id: "Z"
|
||||
title: Uppercase Z
|
||||
94:
|
||||
id: "^"
|
||||
title: Caret
|
||||
95:
|
||||
id: "_"
|
||||
title: Underscore
|
||||
123:
|
||||
id: "{"
|
||||
title: Left Curly Brace
|
||||
124:
|
||||
id: "|"
|
||||
title: Pipe
|
||||
125:
|
||||
id: "}"
|
||||
title: Right Curly Brace
|
||||
126:
|
||||
id: "~"
|
||||
title: Tilde
|
||||
@@ -7,39 +7,9 @@ actions:
|
||||
description: |
|
||||
While SPACE is used for keymaps and chord, just a " " is used in chord outputs.
|
||||
This action is unique in this way. Technically it is "printable", but it is not visible.
|
||||
33:
|
||||
id: "!"
|
||||
title: Exclamation Point
|
||||
34:
|
||||
id: '"'
|
||||
title: Double Quote
|
||||
35:
|
||||
id: "#"
|
||||
title: Hash Symbol
|
||||
36:
|
||||
id: "$"
|
||||
title: Dollar Sign
|
||||
37:
|
||||
id: "%"
|
||||
title: Percent
|
||||
38:
|
||||
id: "&"
|
||||
title: Ampersand
|
||||
39:
|
||||
id: "'"
|
||||
title: Single Quote
|
||||
40:
|
||||
id: "("
|
||||
title: Opening Parenthesis
|
||||
41:
|
||||
id: ")"
|
||||
title: Closing Parenthesis
|
||||
42:
|
||||
id: "*"
|
||||
title: Asterisk
|
||||
43:
|
||||
id: "+"
|
||||
title: Plus
|
||||
44:
|
||||
id: ","
|
||||
title: Comma
|
||||
@@ -82,105 +52,12 @@ actions:
|
||||
57:
|
||||
id: "9"
|
||||
title: Nine
|
||||
58:
|
||||
id: ":"
|
||||
title: Colon
|
||||
59:
|
||||
id: ";"
|
||||
title: Semicolon
|
||||
60:
|
||||
id: "<"
|
||||
title: Less Than
|
||||
61:
|
||||
id: "="
|
||||
title: Equals
|
||||
62:
|
||||
id: ">"
|
||||
title: Greater Than
|
||||
63:
|
||||
id: "?"
|
||||
title: Question Mark
|
||||
64:
|
||||
id: "@"
|
||||
title: At Symbol
|
||||
65:
|
||||
id: "A"
|
||||
title: Uppercase A
|
||||
66:
|
||||
id: "B"
|
||||
title: Uppercase B
|
||||
67:
|
||||
id: "C"
|
||||
title: Uppercase C
|
||||
68:
|
||||
id: "D"
|
||||
title: Uppercase D
|
||||
69:
|
||||
id: "E"
|
||||
title: Uppercase E
|
||||
70:
|
||||
id: "F"
|
||||
title: Uppercase F
|
||||
71:
|
||||
id: "G"
|
||||
title: Uppercase G
|
||||
72:
|
||||
id: "H"
|
||||
title: Uppercase H
|
||||
73:
|
||||
id: "I"
|
||||
title: Uppercase I
|
||||
74:
|
||||
id: "J"
|
||||
title: Uppercase J
|
||||
75:
|
||||
id: "K"
|
||||
title: Uppercase K
|
||||
76:
|
||||
id: "L"
|
||||
title: Uppercase L
|
||||
77:
|
||||
id: "M"
|
||||
title: Uppercase M
|
||||
78:
|
||||
id: "N"
|
||||
title: Uppercase N
|
||||
79:
|
||||
id: "O"
|
||||
title: Uppercase O
|
||||
80:
|
||||
id: "P"
|
||||
title: Uppercase P
|
||||
81:
|
||||
id: "Q"
|
||||
title: Uppercase Q
|
||||
82:
|
||||
id: "R"
|
||||
title: Uppercase R
|
||||
83:
|
||||
id: "S"
|
||||
title: Uppercase S
|
||||
84:
|
||||
id: "T"
|
||||
title: Uppercase T
|
||||
85:
|
||||
id: "U"
|
||||
title: Uppercase U
|
||||
86:
|
||||
id: "V"
|
||||
title: Uppercase V
|
||||
87:
|
||||
id: "W"
|
||||
title: Uppercase W
|
||||
88:
|
||||
id: "X"
|
||||
title: Uppercase X
|
||||
89:
|
||||
id: "Y"
|
||||
title: Uppercase Y
|
||||
90:
|
||||
id: "Z"
|
||||
title: Uppercase Z
|
||||
91:
|
||||
id: "["
|
||||
title: Left Bracket
|
||||
@@ -190,12 +67,6 @@ actions:
|
||||
93:
|
||||
id: "]"
|
||||
title: Right Bracket
|
||||
94:
|
||||
id: "^"
|
||||
title: Caret
|
||||
95:
|
||||
id: "_"
|
||||
title: Underscore
|
||||
96:
|
||||
id: "`"
|
||||
title: Backtick
|
||||
@@ -277,19 +148,6 @@ actions:
|
||||
122:
|
||||
id: "z"
|
||||
title: Lowercase z
|
||||
123:
|
||||
id: "{"
|
||||
title: Left Curly Brace
|
||||
124:
|
||||
id: "|"
|
||||
title: Pipe
|
||||
125:
|
||||
id: "}"
|
||||
title: Right Curly Brace
|
||||
126:
|
||||
id: "~"
|
||||
title: Tilde
|
||||
127:
|
||||
id: "DEL"
|
||||
title: Delete
|
||||
icon: delete_forever
|
||||
|
||||
@@ -6,34 +6,73 @@ type: unassigned
|
||||
actions:
|
||||
600:
|
||||
id: "LH_THUMB_3_3D"
|
||||
title: Left Hand Thumb Top 3D Click
|
||||
title: "Left Hand Thumb Bottom 3D Click"
|
||||
icon: "adjust"
|
||||
601:
|
||||
id: "LH_THUMB_2_3D"
|
||||
title: Left Hand Thumb Middle 3D Click
|
||||
title: "Left Hand Thumb Middle 3D Click"
|
||||
icon: "adjust"
|
||||
602:
|
||||
id: "LH_THUMB_1_3D"
|
||||
title: Left Hand Thumb Bottom 3D Click
|
||||
title: "Left Hand Thumb Top 3D Click"
|
||||
icon: "adjust"
|
||||
603:
|
||||
id: "LH_INDEX_3D"
|
||||
title: Left Hand Index Finger 3D Click
|
||||
title: "Left Hand Index Finger 3D Click"
|
||||
icon: "adjust"
|
||||
604:
|
||||
id: "LH_MID_1_3D"
|
||||
title: Left Hand Middle Finger 3D Click
|
||||
title: "Left Hand Middle Finger 3D Click"
|
||||
icon: "adjust"
|
||||
605:
|
||||
id: "LH_RING_1_3D"
|
||||
title: Left Hand Ring Finger 3D Click
|
||||
title: "Left Hand Ring Finger 3D Click"
|
||||
icon: "adjust"
|
||||
606:
|
||||
id: "LH_PINKY_3D"
|
||||
title: Left Hand Pinky 3D Click,
|
||||
# TODO...
|
||||
# ["607", "CharaChorder One", "LH_MID_2_3D", "", ""],
|
||||
# ["608", "CharaChorder One", "LH_RING_2_3D", "", ""],
|
||||
# ["609", "CharaChorder One", "RH_THUMB_3_3D", "", ""],
|
||||
# ["610", "CharaChorder One", "RH_THUMB_2_3D", "", ""],
|
||||
# ["611", "CharaChorder One", "RH_THUMB_1_3D", "", ""],
|
||||
# ["612", "CharaChorder One", "RH_INDEX_3D", "", ""],
|
||||
# ["613", "CharaChorder One", "RH_MID_1_3D", "", ""],
|
||||
# ["614", "CharaChorder One", "RH_RING_1_3D", "", ""],
|
||||
# ["615", "CharaChorder One", "RH_PINKY_3D", "", ""],
|
||||
# ["616", "CharaChorder One", "RH_MID_2_3D", "", ""],
|
||||
# ["617", "CharaChorder One", "RH_RING_2_3D", "", ""]
|
||||
title: "Left Hand Pinky 3D Click"
|
||||
icon: "adjust"
|
||||
607:
|
||||
id: "LH_MID_2_3D"
|
||||
title: "Left Hand Middle Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
608:
|
||||
id: "LH_RING_2_3D"
|
||||
title: "Left Hand Ring Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
609:
|
||||
id: "RH_THUMB_3_3D"
|
||||
title: "Right Hand Thumb Bottom 3D Click"
|
||||
icon: "adjust"
|
||||
610:
|
||||
id: "RH_THUMB_2_3D"
|
||||
title: "Right Hand Thumb Middle 3D Click"
|
||||
icon: "adjust"
|
||||
611:
|
||||
id: "RH_THUMB_1_3D"
|
||||
title: "Right Hand Thumb Top 3D Click"
|
||||
icon: "adjust"
|
||||
612:
|
||||
id: "RH_INDEX_3D"
|
||||
title: "Right Hand Index Finger 3D Click"
|
||||
icon: "adjust"
|
||||
613:
|
||||
id: "RH_MID_1_3D"
|
||||
title: "Right Hand Middle Finger 3D Click"
|
||||
icon: "adjust"
|
||||
614:
|
||||
id: "RH_RING_1_3D"
|
||||
title: "Right Hand Ring Finger 3D Click"
|
||||
icon: "adjust"
|
||||
615:
|
||||
id: "RH_PINKY_3D"
|
||||
title: "Right Hand Pinky 3D Click"
|
||||
icon: "adjust"
|
||||
616:
|
||||
id: "RH_MID_2_3D"
|
||||
title: "Right Hand Middle Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
617:
|
||||
id: "RH_RING_2_3D"
|
||||
title: "Right Hand Ring Finger 2 3D Click"
|
||||
icon: "adjust"
|
||||
|
||||
@@ -26,7 +26,7 @@ actions:
|
||||
536:
|
||||
id: "DUP"
|
||||
title: Repeat Last Note
|
||||
icon: control_point_duplicate
|
||||
icon: copy_all
|
||||
description: |
|
||||
In character entry, it repeats your last input.
|
||||
In chorded entry, it is used for words with repeating letters.
|
||||
|
||||
@@ -4,9 +4,9 @@ icon: keyboard
|
||||
actions:
|
||||
512: &left_ctrl
|
||||
id: "LEFT_CTRL"
|
||||
display: CTRL
|
||||
title: Control Keyboard Modifier
|
||||
variant: left
|
||||
icon: keyboard_control_key
|
||||
513: &left_shift
|
||||
id: "LEFT_SHIFT"
|
||||
title: Shift Keyboard Modifier
|
||||
@@ -14,14 +14,14 @@ actions:
|
||||
icon: shift
|
||||
514: &left_alt
|
||||
id: "LEFT_ALT"
|
||||
display: ALT
|
||||
title: Alt Keyboard Modifier
|
||||
variant: left
|
||||
icon: keyboard_option_key
|
||||
515: &left_gui
|
||||
id: "LEFT_GUI"
|
||||
title: GUI Keyboard Modifier
|
||||
icon: apps
|
||||
variant: left
|
||||
icon: keyboard_command_key
|
||||
516:
|
||||
variationOf: 512
|
||||
<<: *left_ctrl
|
||||
@@ -31,14 +31,17 @@ actions:
|
||||
variationOf: 513
|
||||
<<: *left_shift
|
||||
id: "RIGHT_SHIFT"
|
||||
variant: right
|
||||
518:
|
||||
variationOf: 514
|
||||
<<: *left_alt
|
||||
id: "RIGHT_ALT"
|
||||
variant: right
|
||||
519:
|
||||
variationOf: 515
|
||||
<<: *left_gui
|
||||
id: "RIGHT_GUI"
|
||||
variant: right
|
||||
520:
|
||||
id: "RELEASE_MOD"
|
||||
title: Release all keyboard modifiers
|
||||
@@ -51,3 +54,11 @@ actions:
|
||||
id: "RELEASE_KEYS"
|
||||
title: Release all keys, but not keyboard modifiers
|
||||
icon: text_rotate_up
|
||||
523:
|
||||
id: "PRESS_NEXT"
|
||||
title: "Press and do not release the next key/action"
|
||||
icon: download
|
||||
524:
|
||||
id: "RELEASE_NEXT"
|
||||
title: "Release the next key/action in the sequence"
|
||||
icon: upload
|
||||
|
||||
3
src/lib/assets/keymaps/keymap.d.ts
vendored
3
src/lib/assets/keymaps/keymap.d.ts
vendored
@@ -2,6 +2,7 @@ export interface KeymapCategory {
|
||||
name: string
|
||||
description: string
|
||||
icon?: string
|
||||
display?: string
|
||||
type?: "unassigned"
|
||||
actions: Record<number, Partial<ActionInfo>>
|
||||
}
|
||||
@@ -10,7 +11,9 @@ export interface ActionInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
display: string
|
||||
description: string
|
||||
variant: "left" | "right"
|
||||
variantOf: number
|
||||
keyCode: string
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
name: Raw Scancodes
|
||||
description: Raw Keyboard Scancodes
|
||||
name: Key codes
|
||||
description: OS-Layout sensitive keycodes
|
||||
actions:
|
||||
256:
|
||||
id: "KSC_00"
|
||||
icon: block
|
||||
title: No Key Pressed
|
||||
description: Also commonly used at the end of a chord to remove auto-spaces
|
||||
257:
|
||||
id: "KSC_01"
|
||||
title: Keyboard Error Roll Over
|
||||
@@ -15,311 +17,408 @@ actions:
|
||||
title: Keyboard Error Undefined
|
||||
260:
|
||||
id: "KEY_A"
|
||||
keyCode: "KeyA"
|
||||
title: Keyboard a and A (US English)
|
||||
description: Non US English keyboard users may prefer these Raw Scancodes
|
||||
261:
|
||||
id: "KEY_B"
|
||||
keyCode: "KeyB"
|
||||
title: Keyboard b and B (US English)
|
||||
262:
|
||||
id: "KEY_C"
|
||||
keyCode: "KeyC"
|
||||
title: Keyboard c and C (US English)
|
||||
263:
|
||||
id: "KEY_D"
|
||||
keyCode: "KeyD"
|
||||
title: Keyboard d and D (US English)
|
||||
264:
|
||||
id: "KEY_E"
|
||||
keyCode: "KeyE"
|
||||
title: Keyboard e and E (US English)
|
||||
265:
|
||||
id: "KEY_F"
|
||||
keyCode: "KeyF"
|
||||
title: Keyboard f and F (US English)
|
||||
266:
|
||||
id: "KEY_G"
|
||||
keyCode: "KeyG"
|
||||
title: Keyboard g and G (US English)
|
||||
267:
|
||||
id: "KEY_H"
|
||||
keyCode: "KeyH"
|
||||
title: Keyboard h and H (US English)
|
||||
268:
|
||||
id: "KEY_I"
|
||||
keyCode: "KeyI"
|
||||
title: Keyboard i and I (US English)
|
||||
269:
|
||||
id: "KEY_J"
|
||||
keyCode: "KeyJ"
|
||||
title: Keyboard j and J (US English)
|
||||
270:
|
||||
id: "KEY_K"
|
||||
keyCode: "KeyK"
|
||||
title: Keyboard k and K (US English)
|
||||
271:
|
||||
id: "KEY_L"
|
||||
keyCode: "KeyL"
|
||||
title: Keyboard l and L (US English)
|
||||
272:
|
||||
id: "KEY_M"
|
||||
keyCode: "KeyM"
|
||||
title: Keyboard m and M (US English)
|
||||
273:
|
||||
id: "KEY_N"
|
||||
keyCode: "KeyN"
|
||||
title: Keyboard n and N (US English)
|
||||
274:
|
||||
id: "KEY_O"
|
||||
keyCode: "KeyO"
|
||||
title: Keyboard o and O (US English)
|
||||
275:
|
||||
id: "KEY_P"
|
||||
keyCode: "KeyP"
|
||||
title: Keyboard p and P (US English)
|
||||
276:
|
||||
id: "KEY_Q"
|
||||
keyCode: "KeyQ"
|
||||
title: Keyboard q and Q (US English)
|
||||
277:
|
||||
id: "KEY_R"
|
||||
keyCode: "KeyR"
|
||||
title: Keyboard r and R (US English)
|
||||
278:
|
||||
id: "KEY_S"
|
||||
keyCode: "KeyS"
|
||||
title: Keyboard s and S (US English)
|
||||
279:
|
||||
id: "KEY_T"
|
||||
keyCode: "KeyT"
|
||||
title: Keyboard t and T (US English)
|
||||
280:
|
||||
id: "KEY_U"
|
||||
keyCode: "KeyU"
|
||||
title: Keyboard u and U (US English)
|
||||
281:
|
||||
id: "KEY_V"
|
||||
keyCode: "KeyV"
|
||||
title: Keyboard v and V (US English)
|
||||
282:
|
||||
id: "KEY_W"
|
||||
keyCode: "KeyW"
|
||||
title: Keyboard w and W (US English)
|
||||
283:
|
||||
id: "KEY_X"
|
||||
keyCode: "KeyX"
|
||||
title: Keyboard x and X (US English)
|
||||
284:
|
||||
id: "KEY_Y"
|
||||
keyCode: "KeyY"
|
||||
title: Keyboard y and Y (US English)
|
||||
285:
|
||||
id: "KEY_Z"
|
||||
keyCode: "KeyZ"
|
||||
title: Keyboard z and Z (US English)
|
||||
286:
|
||||
id: "KEY_1"
|
||||
keyCode: "Digit1"
|
||||
title: Keyboard 1 and ! (US English)
|
||||
287:
|
||||
id: "KEY_2"
|
||||
keyCode: "Digit2"
|
||||
title: Keyboard 2 and @ (US English)
|
||||
288:
|
||||
id: "KEY_3"
|
||||
keyCode: "Digit3"
|
||||
title: Keyboard 3 and # (US English)
|
||||
289:
|
||||
id: "KEY_4"
|
||||
keyCode: "Digit4"
|
||||
title: Keyboard 4 and $ (US English)
|
||||
290:
|
||||
id: "KEY_5"
|
||||
keyCode: "Digit5"
|
||||
title: Keyboard 5 and % (US English)
|
||||
291:
|
||||
id: "KEY_6"
|
||||
keyCode: "Digit6"
|
||||
title: Keyboard 6 and ^ (US English)
|
||||
292:
|
||||
id: "KEY_7"
|
||||
keyCode: "Digit7"
|
||||
title: Keyboard 7 and & (US English)
|
||||
293:
|
||||
id: "KEY_8"
|
||||
keyCode: "Digit8"
|
||||
title: Keyboard 8 and * (US English)
|
||||
294:
|
||||
id: "KEY_9"
|
||||
keyCode: "Digit9"
|
||||
title: Keyboard 9 and ( (US English)
|
||||
295:
|
||||
id: "KEY_0"
|
||||
keyCode: "Digit0"
|
||||
title: Keyboard 0 and ) (US English)
|
||||
296:
|
||||
id: "ENTER"
|
||||
keyCode: "Enter"
|
||||
title: Keyboard Return (US English)
|
||||
icon: keyboard_return
|
||||
297:
|
||||
id: "ESC"
|
||||
keyCode: "Escape"
|
||||
title: Keyboard Escape (US English)
|
||||
298:
|
||||
id: "BKSP"
|
||||
keyCode: "Backspace"
|
||||
title: Keyboard Backspace (US English)
|
||||
icon: backspace
|
||||
299:
|
||||
id: "TAB"
|
||||
keyCode: "Tab"
|
||||
title: Keyboard Tab (US English)
|
||||
icon: keyboard_tab
|
||||
300:
|
||||
id: "KSC_2C"
|
||||
keyCode: "Space"
|
||||
title: Keyboard Space (US English)
|
||||
description: |
|
||||
The ASCII space is preferred over this raw scancode for the space bar.
|
||||
icon: space_bar
|
||||
301:
|
||||
id: "KSC_2D"
|
||||
keyCode: "Minus"
|
||||
title: Keyboard - and _ (US English)
|
||||
302:
|
||||
id: "KSC_2E"
|
||||
keyCode: "Equal"
|
||||
title: Keyboard = and + (US English)
|
||||
303:
|
||||
id: "KSC_2F"
|
||||
keyCode: "BracketLeft"
|
||||
title: Keyboard [ and { (US English)
|
||||
304:
|
||||
id: "KSC_30"
|
||||
keyCode: "BracketRight"
|
||||
title: Keyboard ] and } (US English)
|
||||
305:
|
||||
id: "KSC_31"
|
||||
keyCode: "Backslash"
|
||||
title: Keyboard \ and | (US English)
|
||||
306:
|
||||
id: "KSC_32"
|
||||
# TODO: also backslash?
|
||||
title: Keyboard Non-US \# and ~ (US English)
|
||||
307:
|
||||
id: "KSC_33"
|
||||
keyCode: "Semicolon"
|
||||
title: "Keyboard ; and : (US English)"
|
||||
308:
|
||||
id: "KSC_34"
|
||||
keyCode: "Quote"
|
||||
title: Keyboard ' and " (US English)
|
||||
309:
|
||||
id: "KSC_35"
|
||||
keyCode: "Backquote"
|
||||
title: Keyboard ` and ~ (US English)
|
||||
310:
|
||||
id: "KSC_36"
|
||||
keyCode: "Comma"
|
||||
title: Keyboard , and < (US English)
|
||||
311:
|
||||
id: "KSC_37"
|
||||
keyCode: "Period"
|
||||
title: Keyboard . and > (US English)
|
||||
312:
|
||||
id: "KSC_38"
|
||||
keyCode: "Slash"
|
||||
title: Keyboard / and ? (US English)
|
||||
313:
|
||||
id: "CAPSLOCK"
|
||||
keyCode: "CapsLock"
|
||||
title: Keyboard Caps Lock
|
||||
icon: shift_lock
|
||||
314:
|
||||
id: "F1"
|
||||
keyCode: "F1"
|
||||
title: Keyboard F1
|
||||
315:
|
||||
id: "F2"
|
||||
keyCode: "F2"
|
||||
title: Keyboard F2
|
||||
316:
|
||||
id: "F3"
|
||||
keyCode: "F3"
|
||||
title: Keyboard F3
|
||||
317:
|
||||
id: "F4"
|
||||
keyCode: "F4"
|
||||
title: Keyboard F4
|
||||
318:
|
||||
id: "F5"
|
||||
keyCode: "F5"
|
||||
title: Keyboard F5
|
||||
319:
|
||||
id: "F6"
|
||||
keyCode: "F6"
|
||||
title: Keyboard F6
|
||||
320:
|
||||
id: "F7"
|
||||
keyCode: "F7"
|
||||
title: Keyboard F7
|
||||
321:
|
||||
id: "F8"
|
||||
keyCode: "F8"
|
||||
title: Keyboard F8
|
||||
322:
|
||||
id: "F9"
|
||||
keyCode: "F9"
|
||||
title: Keyboard F9
|
||||
323:
|
||||
id: "F10"
|
||||
keyCode: "F10"
|
||||
title: Keyboard F10
|
||||
324:
|
||||
id: "F11"
|
||||
keyCode: "F11"
|
||||
title: Keyboard F11
|
||||
325:
|
||||
id: "F12"
|
||||
keyCode: "F12"
|
||||
title: Keyboard F12
|
||||
326:
|
||||
id: "PRTSCN"
|
||||
keyCode: "PrintScreen"
|
||||
title: Keyboard Print Screen
|
||||
icon: screenshot_monitor
|
||||
327:
|
||||
id: "SCRLK"
|
||||
keyCode: "ScrollLock"
|
||||
title: Keyboard Scroll Lock
|
||||
328:
|
||||
id: "PAUSE"
|
||||
keyCode: "Pause"
|
||||
title: Keyboard Pause
|
||||
329:
|
||||
id: "INSERT"
|
||||
keyCode: "Insert"
|
||||
title: Keyboard Insert
|
||||
icon: insert_text
|
||||
330:
|
||||
id: "HOME"
|
||||
keyCode: "Home"
|
||||
title: Keyboard Home
|
||||
icon: home
|
||||
331:
|
||||
id: "PGUP"
|
||||
keyCode: "PageUp"
|
||||
title: Keyboard Page Up
|
||||
icon: move_up
|
||||
332:
|
||||
id: "DELETE"
|
||||
keyCode: "Delete"
|
||||
title: Keyboard Delete Forward
|
||||
333:
|
||||
id: "END"
|
||||
keyCode: "End"
|
||||
title: Keyboard End
|
||||
334:
|
||||
id: "PGDN"
|
||||
keyCode: "PageDown"
|
||||
title: Keyboard Page Down
|
||||
icon: move_down
|
||||
335:
|
||||
id: "ARROW_RT"
|
||||
keyCode: "ArrowRight"
|
||||
title: Keyboard Right Arrow
|
||||
icon: keyboard_arrow_right
|
||||
336:
|
||||
id: "ARROW_LF"
|
||||
keyCode: "ArrowLeft"
|
||||
title: Keyboard Left Arrow
|
||||
icon: keyboard_arrow_left
|
||||
337:
|
||||
id: "ARROW_DN"
|
||||
keyCode: "ArrowDown"
|
||||
title: Keyboard Down Arrow
|
||||
icon: keyboard_arrow_down
|
||||
338:
|
||||
id: "ARROW_UP"
|
||||
keyCode: "ArrowUp"
|
||||
title: Keyboard Up Arrow
|
||||
icon: keyboard_arrow_up
|
||||
339:
|
||||
id: "NUMLOCK"
|
||||
keyCode: "NumLock"
|
||||
title: Keyboard Num Lock and Clear
|
||||
340:
|
||||
id: "KP_SLASH"
|
||||
keyCode: "NumpadDivide"
|
||||
title: Keypad /
|
||||
341:
|
||||
id: "KP_ASTER"
|
||||
keyCode: "NumpadStar"
|
||||
title: Keypad *
|
||||
342:
|
||||
id: "KP_MINUS"
|
||||
keyCode: "NumpadSubtract"
|
||||
title: Keypad -
|
||||
343:
|
||||
id: "KP_PLUS"
|
||||
keyCode: "NumpadAdd"
|
||||
title: Keypad +
|
||||
344:
|
||||
id: "KP_ENTER"
|
||||
keyCode: "NumpadEnter"
|
||||
title: Keypad Enter
|
||||
345:
|
||||
id: "KP_1"
|
||||
keyCode: "Numpad1"
|
||||
title: Keypad 1 and End
|
||||
346:
|
||||
id: "KP_2"
|
||||
keyCode: "Numpad2"
|
||||
title: Keypad 2 and Down Arrow
|
||||
347:
|
||||
id: "KP_3"
|
||||
keyCode: "Numpad3"
|
||||
title: Keypad 3 and Page Down
|
||||
348:
|
||||
id: "KP_4"
|
||||
keyCode: "Numpad4"
|
||||
title: Keypad 4 and Left Arrow
|
||||
349:
|
||||
id: "KP_5"
|
||||
keyCode: "Numpad5"
|
||||
title: Keypad 5
|
||||
350:
|
||||
id: "KP_6"
|
||||
keyCode: "Numpad6"
|
||||
title: Keypad 6 and Rigth Arrow
|
||||
351:
|
||||
id: "KP_7"
|
||||
keyCode: "Numpad7"
|
||||
title: Keypad 7 and Home
|
||||
352:
|
||||
id: "KP_8"
|
||||
keyCode: "Numpad8"
|
||||
title: Keypad 8 and Up Arrow
|
||||
353:
|
||||
id: "KP_9"
|
||||
keyCode: "Numpad9"
|
||||
title: Keypad 9 and Page Up
|
||||
354:
|
||||
id: "KP_0"
|
||||
keyCode: "Numpad0"
|
||||
title: Keypad 0 and Insert
|
||||
355:
|
||||
id: "KP_DOT"
|
||||
keyCode: "NumpadDecimal"
|
||||
title: Keypad . and Delete
|
||||
356:
|
||||
id: "KSC_64"
|
||||
keyCode: "IntlBackslash"
|
||||
title: Keyboard Non-US \ and | (US English)
|
||||
357:
|
||||
id: "COMPOSE"
|
||||
@@ -327,10 +426,12 @@ actions:
|
||||
description: Officially supported by Win, Unix, and Boot
|
||||
358:
|
||||
id: "POWER"
|
||||
keyCode: "Power"
|
||||
title: Keyboard Power
|
||||
description: Only officially supported by Mac and Unix
|
||||
359:
|
||||
id: "KP_EQUAL"
|
||||
keyCode: "NumpadEqual"
|
||||
title: Keypad =
|
||||
description: Only officially supported by Mac
|
||||
360:
|
||||
@@ -787,10 +888,12 @@ actions:
|
||||
description: Not required to be supported by any OS
|
||||
472:
|
||||
id: "KSC_D8"
|
||||
keyCode: "NumpadClear"
|
||||
title: Keypad Clear
|
||||
description: Not required to be supported by any OS
|
||||
473:
|
||||
id: "KSC_D9"
|
||||
keyCode: "NumpadClearEntry"
|
||||
title: Keypad Clear Entry
|
||||
description: Not required to be supported by any OS
|
||||
474:
|
||||
@@ -817,58 +920,74 @@ actions:
|
||||
description: Not required to be supported by any OS
|
||||
480:
|
||||
id: "KSC_E0"
|
||||
keyCode: "ControlLeft"
|
||||
title: Keyboard Left Control
|
||||
481:
|
||||
id: "KSC_E1"
|
||||
keyCode: "ShiftLeft"
|
||||
title: Keyboard Left Shift
|
||||
482:
|
||||
id: "KSC_E2"
|
||||
keyCode: "AltLeft"
|
||||
title: Keyboard Left Alt
|
||||
483:
|
||||
id: "KSC_E3"
|
||||
keyCode: "MetaLeft"
|
||||
title: Keyboard Left GUI
|
||||
484:
|
||||
id: "KSC_E4"
|
||||
keyCode: "ControlRight"
|
||||
title: Keyboard Right Control
|
||||
485:
|
||||
id: "KSC_E5"
|
||||
keyCode: "ShiftRight"
|
||||
title: Keyboard Right Shift
|
||||
486:
|
||||
id: "KSC_E6"
|
||||
keyCode: "AltRight"
|
||||
title: Keyboard Right Alt
|
||||
487:
|
||||
id: "KSC_E7"
|
||||
keyCode: "MetaRight"
|
||||
title: Keyboard Right GUI
|
||||
488:
|
||||
id: "KSC_E8"
|
||||
keyCode: "MediaPlayPause"
|
||||
title: Media Play Pause
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
489:
|
||||
id: "KSC_E9"
|
||||
keyCode: "MediaStop"
|
||||
title: Media Stop CD
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
490:
|
||||
id: "KSC_EA"
|
||||
keyCode: "MediaTrackPrevious"
|
||||
title: Media Previous Song
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
491:
|
||||
id: "KSC_EB"
|
||||
keyCode: "MediaTrackNext"
|
||||
title: Media Next Song
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
492:
|
||||
id: "KSC_EC"
|
||||
keyCode: "Eject"
|
||||
title: Media Eject CD
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
493:
|
||||
id: "KSC_ED"
|
||||
keyCode: "AudioVolumeUp"
|
||||
title: Media Volume Up
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
494:
|
||||
id: "KSC_EE"
|
||||
keyCode: "AudioVolumeDown"
|
||||
title: Media Volume Down
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
495:
|
||||
id: "KSC_EF"
|
||||
keyCode: "AudioVolumeMute"
|
||||
title: Media Mute
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
496:
|
||||
@@ -877,18 +996,22 @@ actions:
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
497:
|
||||
id: "KSC_F1"
|
||||
keyCode: "BrowserBack"
|
||||
title: Media Back
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
498:
|
||||
id: "KSC_F2"
|
||||
keyCode: "BrowserForward"
|
||||
title: Media Forward
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
499:
|
||||
id: "KSC_F3"
|
||||
keyCode: "BrowserStop"
|
||||
title: Media Stop
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
500:
|
||||
id: "KSC_F4"
|
||||
keyCode: "BrowserSearch"
|
||||
title: Media Find
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
501:
|
||||
@@ -905,14 +1028,17 @@ actions:
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
504:
|
||||
id: "KSC_F8"
|
||||
keyCode: "Sleep"
|
||||
title: Media Sleep
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
505:
|
||||
id: "KSC_F9"
|
||||
keyCode: "WakeUp"
|
||||
title: Media Coffee
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
506:
|
||||
id: "KSC_FA"
|
||||
keyCode: "BrowserRefresh"
|
||||
title: Media Refresh
|
||||
description: Not required to be supported by any OS. Possibly deprecated.
|
||||
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
|
||||
87
src/lib/assets/layouts/lite.yml
Normal file
87
src/lib/assets/layouts/lite.yml
Normal file
@@ -0,0 +1,87 @@
|
||||
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 |
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
|
||||
164
src/lib/backup/backup.ts
Normal file
164
src/lib/backup/backup.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
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 {
|
||||
alert("Unknown backup format")
|
||||
}
|
||||
}
|
||||
|
||||
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[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
|
||||
28
src/lib/backup/compat/legacy-chords.ts
Normal file
28
src/lib/backup/compat/legacy-chords.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {KEYMAP_IDS} from "$lib/serial/keymap-codes"
|
||||
import type {CharaChordFile} from "$lib/share/chara-file"
|
||||
|
||||
const SPECIAL_KEYS = new Map<string, string>([[" ", "SPACE"]])
|
||||
|
||||
export function csvChordsToJson(csv: string): CharaChordFile {
|
||||
return {
|
||||
charaVersion: 1,
|
||||
type: "chords",
|
||||
chords: csv
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map(line => {
|
||||
const [input, output] = line.split(/,(?=[^,]*$)/, 2)
|
||||
return [
|
||||
input.split("+").map(it => KEYMAP_IDS.get(it.trim())?.code ?? 0),
|
||||
output
|
||||
.trim()
|
||||
.split("")
|
||||
.map(it => KEYMAP_IDS.get(SPECIAL_KEYS.get(it) ?? it)?.code ?? 0),
|
||||
]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function isCsvChords(csv: string): boolean {
|
||||
return /^([^+]+( *\+ *[^+]+)* *, *[^+, ]+ *(\n|(?=$)))+$/.test(csv)
|
||||
}
|
||||
24
src/lib/backup/compat/legacy-layout-converted.sample.json
Normal file
24
src/lib/backup/compat/legacy-layout-converted.sample.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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
|
||||
]
|
||||
]
|
||||
}
|
||||
270
src/lib/backup/compat/legacy-layout.sample.csv
Normal file
270
src/lib/backup/compat/legacy-layout.sample.csv
Normal file
@@ -0,0 +1,270 @@
|
||||
A1,0,309
|
||||
A1,1,304
|
||||
A1,2,312
|
||||
A1,3,303
|
||||
A1,4,306
|
||||
A1,5,290
|
||||
A1,6,282
|
||||
A1,7,301
|
||||
A1,8,266
|
||||
A1,9,285
|
||||
A1,10,289
|
||||
A1,11,270
|
||||
A1,12,281
|
||||
A1,13,272
|
||||
A1,14,262
|
||||
A1,15,288
|
||||
A1,16,277
|
||||
A1,17,298
|
||||
A1,18,307
|
||||
A1,19,264
|
||||
A1,20,287
|
||||
A1,21,268
|
||||
A1,22,332
|
||||
A1,23,311
|
||||
A1,24,274
|
||||
A1,25,286
|
||||
A1,26,308
|
||||
A1,27,329
|
||||
A1,28,310
|
||||
A1,29,280
|
||||
A1,30,358
|
||||
A1,31,512
|
||||
A1,32,515
|
||||
A1,33,513
|
||||
A1,34,514
|
||||
A1,35,313
|
||||
A1,36,319
|
||||
A1,37,318
|
||||
A1,38,321
|
||||
A1,39,320
|
||||
A1,40,326
|
||||
A1,41,315
|
||||
A1,42,314
|
||||
A1,43,317
|
||||
A1,44,316
|
||||
A1,45,312
|
||||
A1,46,330
|
||||
A1,47,331
|
||||
A1,48,333
|
||||
A1,49,334
|
||||
A1,50,291
|
||||
A1,51,261
|
||||
A1,52,283
|
||||
A1,53,536
|
||||
A1,54,276
|
||||
A1,55,292
|
||||
A1,56,265
|
||||
A1,57,275
|
||||
A1,58,267
|
||||
A1,59,263
|
||||
A1,60,293
|
||||
A1,61,260
|
||||
A1,62,296
|
||||
A1,63,544
|
||||
A1,64,279
|
||||
A1,65,294
|
||||
A1,66,271
|
||||
A1,67,299
|
||||
A1,68,269
|
||||
A1,69,273
|
||||
A1,70,295
|
||||
A1,71,284
|
||||
A1,72,297
|
||||
A1,73,302
|
||||
A1,74,278
|
||||
A1,75,357
|
||||
A1,76,516
|
||||
A1,77,519
|
||||
A1,78,517
|
||||
A1,79,518
|
||||
A1,80,327
|
||||
A1,81,336
|
||||
A1,82,338
|
||||
A1,83,335
|
||||
A1,84,337
|
||||
A1,85,328
|
||||
A1,86,325
|
||||
A1,87,322
|
||||
A1,88,323
|
||||
A1,89,324
|
||||
A2,0,0
|
||||
A2,1,0
|
||||
A2,2,0
|
||||
A2,3,0
|
||||
A2,4,0
|
||||
A2,5,0
|
||||
A2,6,0
|
||||
A2,7,0
|
||||
A2,8,0
|
||||
A2,9,0
|
||||
A2,10,0
|
||||
A2,11,0
|
||||
A2,12,0
|
||||
A2,13,0
|
||||
A2,14,0
|
||||
A2,15,0
|
||||
A2,16,0
|
||||
A2,17,0
|
||||
A2,18,0
|
||||
A2,19,0
|
||||
A2,20,0
|
||||
A2,21,0
|
||||
A2,22,0
|
||||
A2,23,0
|
||||
A2,24,0
|
||||
A2,25,0
|
||||
A2,26,0
|
||||
A2,27,0
|
||||
A2,28,0
|
||||
A2,29,0
|
||||
A2,30,0
|
||||
A2,31,0
|
||||
A2,32,0
|
||||
A2,33,0
|
||||
A2,34,0
|
||||
A2,35,0
|
||||
A2,36,0
|
||||
A2,37,0
|
||||
A2,38,0
|
||||
A2,39,0
|
||||
A2,40,0
|
||||
A2,41,0
|
||||
A2,42,0
|
||||
A2,43,0
|
||||
A2,44,0
|
||||
A2,45,0
|
||||
A2,46,0
|
||||
A2,47,0
|
||||
A2,48,0
|
||||
A2,49,0
|
||||
A2,50,0
|
||||
A2,51,0
|
||||
A2,52,0
|
||||
A2,53,0
|
||||
A2,54,0
|
||||
A2,55,0
|
||||
A2,56,0
|
||||
A2,57,0
|
||||
A2,58,0
|
||||
A2,59,0
|
||||
A2,60,0
|
||||
A2,61,0
|
||||
A2,62,0
|
||||
A2,63,0
|
||||
A2,64,0
|
||||
A2,65,0
|
||||
A2,66,0
|
||||
A2,67,0
|
||||
A2,68,0
|
||||
A2,69,0
|
||||
A2,70,0
|
||||
A2,71,0
|
||||
A2,72,0
|
||||
A2,73,0
|
||||
A2,74,0
|
||||
A2,75,0
|
||||
A2,76,0
|
||||
A2,77,0
|
||||
A2,78,0
|
||||
A2,79,0
|
||||
A2,80,0
|
||||
A2,81,0
|
||||
A2,82,0
|
||||
A2,83,0
|
||||
A2,84,0
|
||||
A2,85,0
|
||||
A2,86,0
|
||||
A2,87,0
|
||||
A2,88,0
|
||||
A2,89,0
|
||||
A3,0,0
|
||||
A3,1,0
|
||||
A3,2,0
|
||||
A3,3,0
|
||||
A3,4,0
|
||||
A3,5,0
|
||||
A3,6,0
|
||||
A3,7,0
|
||||
A3,8,0
|
||||
A3,9,0
|
||||
A3,10,0
|
||||
A3,11,0
|
||||
A3,12,0
|
||||
A3,13,0
|
||||
A3,14,0
|
||||
A3,15,0
|
||||
A3,16,0
|
||||
A3,17,0
|
||||
A3,18,0
|
||||
A3,19,0
|
||||
A3,20,0
|
||||
A3,21,0
|
||||
A3,22,0
|
||||
A3,23,0
|
||||
A3,24,0
|
||||
A3,25,0
|
||||
A3,26,0
|
||||
A3,27,0
|
||||
A3,28,0
|
||||
A3,29,0
|
||||
A3,30,0
|
||||
A3,31,0
|
||||
A3,32,0
|
||||
A3,33,0
|
||||
A3,34,0
|
||||
A3,35,0
|
||||
A3,36,0
|
||||
A3,37,0
|
||||
A3,38,0
|
||||
A3,39,0
|
||||
A3,40,0
|
||||
A3,41,0
|
||||
A3,42,0
|
||||
A3,43,0
|
||||
A3,44,0
|
||||
A3,45,0
|
||||
A3,46,0
|
||||
A3,47,0
|
||||
A3,48,0
|
||||
A3,49,0
|
||||
A3,50,0
|
||||
A3,51,0
|
||||
A3,52,0
|
||||
A3,53,0
|
||||
A3,54,0
|
||||
A3,55,0
|
||||
A3,56,0
|
||||
A3,57,0
|
||||
A3,58,0
|
||||
A3,59,0
|
||||
A3,60,0
|
||||
A3,61,0
|
||||
A3,62,0
|
||||
A3,63,0
|
||||
A3,64,0
|
||||
A3,65,0
|
||||
A3,66,0
|
||||
A3,67,0
|
||||
A3,68,0
|
||||
A3,69,0
|
||||
A3,70,0
|
||||
A3,71,0
|
||||
A3,72,0
|
||||
A3,73,0
|
||||
A3,74,0
|
||||
A3,75,0
|
||||
A3,76,0
|
||||
A3,77,0
|
||||
A3,78,0
|
||||
A3,79,0
|
||||
A3,80,0
|
||||
A3,81,0
|
||||
A3,82,0
|
||||
A3,83,0
|
||||
A3,84,0
|
||||
A3,85,0
|
||||
A3,86,0
|
||||
A3,87,0
|
||||
A3,88,0
|
||||
A3,89,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)
|
||||
})
|
||||
})
|
||||
25
src/lib/backup/compat/legacy-layout.ts
Normal file
25
src/lib/backup/compat/legacy-layout.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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,23 +0,0 @@
|
||||
import type {Chord} from "$lib/serial/chord"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
|
||||
interface Language {
|
||||
name: string
|
||||
noLazyMode?: boolean
|
||||
orderedByFrequency?: boolean
|
||||
words: string[]
|
||||
}
|
||||
|
||||
export async function calculateChordCoverage(chords: Chord[]) {
|
||||
const language: Language = await fetch("/languages/english.json").then(it => it.json())
|
||||
|
||||
const words = new Set(language.words)
|
||||
for (const chord of chords) {
|
||||
words.delete(chord.phrase.map(it => KEYMAP_CODES[it].id!).join(""))
|
||||
}
|
||||
|
||||
return {
|
||||
coverage: words.size / language.words.length,
|
||||
missing: [...words.values()],
|
||||
}
|
||||
}
|
||||
84
src/lib/components/Action.svelte
Normal file
84
src/lib/components/Action.svelte
Normal file
@@ -0,0 +1,84 @@
|
||||
<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[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,64 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||
|
||||
export let exact: number | undefined = undefined
|
||||
export let code: number = Number.NaN
|
||||
export let results: number[] = []
|
||||
|
||||
export let width: number
|
||||
</script>
|
||||
|
||||
<div class="list" style="width: {width}px">
|
||||
{#if exact !== undefined}
|
||||
<div class="exact">
|
||||
<i>Exact match</i>
|
||||
<ActionListItem id={exact} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if !exact && code}
|
||||
{#if code >= 2 ** 5 && code < 2 ** 13}
|
||||
<button>USE CODE</button>
|
||||
{:else}
|
||||
<div>Action code is out of range</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#each results as id (id)}
|
||||
<ActionListItem {id} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.list {
|
||||
--scrollbar-color: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
scrollbar-gutter: stable both-edges;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-height: 500px;
|
||||
padding-block: 8px;
|
||||
}
|
||||
|
||||
.exact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
border: 1px solid var(--md-sys-color-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
> i {
|
||||
padding-inline: 8px;
|
||||
color: var(--md-sys-color-on-primary);
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||
import LL from "../../i18n/i18n-svelte"
|
||||
import Action from "$lib/components/Action.svelte"
|
||||
|
||||
export let id: number | KeyInfo
|
||||
|
||||
$: key = (typeof id === "number" ? KEYMAP_CODES[id] ?? id : id) as number | KeyInfo
|
||||
</script>
|
||||
|
||||
<button>
|
||||
<button on:click>
|
||||
{#if typeof key === "object"}
|
||||
<div class="title">
|
||||
<b>
|
||||
@@ -21,8 +23,14 @@
|
||||
{#if key.description}
|
||||
<i>{key.description}</i>
|
||||
{/if}
|
||||
{#if key.category.name === "ASCII Macros"}
|
||||
<span class="warning">{@html $LL.actionSearch.SHIFT_WARNING()}</span>
|
||||
{/if}
|
||||
{#if key.category.name === "CP-1252"}
|
||||
<span class="warning">{@html $LL.actionSearch.ALT_CODE_WARNING()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class:icon={!!key.icon} class="key">{key.icon || key.id || `0x${key.code.toString(16)}`}</span>
|
||||
<Action display="keys" action={key} />
|
||||
{:else}
|
||||
<span class="key">0x{key.toString(16)}</span>
|
||||
{/if}
|
||||
@@ -35,6 +43,7 @@
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
|
||||
@@ -43,6 +52,13 @@
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
|
||||
&:focus-visible {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -55,17 +71,14 @@
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.key {
|
||||
.warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--md-sys-color-error);
|
||||
|
||||
min-width: 32px;
|
||||
padding: 4px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 4px;
|
||||
> :global(.icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</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}
|
||||
@@ -5,7 +5,9 @@
|
||||
</script>
|
||||
|
||||
{#if $needRefresh}
|
||||
<button title="Update ready" class="icon" on:click={() => updateServiceWorker(true)}>update</button>
|
||||
<button title="Update ready" on:click={() => updateServiceWorker(true)}
|
||||
>Update <span class="icon">update</span></button
|
||||
>
|
||||
{:else if $offlineReady}
|
||||
<div title="App can now be used offline" class="icon">offline_pin</div>
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {serialLog, serialPort} from "$lib/serial/connection"
|
||||
import {slide} from "svelte/transition"
|
||||
|
||||
function submit(event: InputEvent) {
|
||||
function submit(event: Event) {
|
||||
event.preventDefault()
|
||||
$serialPort.send(value.trim())
|
||||
value = ""
|
||||
@@ -16,7 +16,13 @@
|
||||
<form on:submit={submit}>
|
||||
<div bind:this={io} class="io">
|
||||
{#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}
|
||||
<div class="anchor" />
|
||||
</div>
|
||||
@@ -111,17 +117,15 @@
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
code,
|
||||
samp,
|
||||
p {
|
||||
display: block;
|
||||
overflow-anchor: none;
|
||||
margin-block: 0.15rem;
|
||||
}
|
||||
|
||||
p.input {
|
||||
margin-block-end: 0.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p.system {
|
||||
p {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -134,8 +138,9 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
p.input::before {
|
||||
code::before {
|
||||
content: "> ";
|
||||
margin-block-end: 0.25rem;
|
||||
font-weight: 900;
|
||||
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,77 +0,0 @@
|
||||
<script lang="ts">
|
||||
import LayoutCC1 from "$lib/components/layout/LayoutCC1.svelte"
|
||||
import {chords, highlightActions} from "$lib/serial/connection"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
||||
|
||||
$: content = Array.from({length: 10}).map(() => $chords[Math.floor(Math.random() * $chords.length)])
|
||||
|
||||
let cursor = [0, 0]
|
||||
let input = []
|
||||
|
||||
$: {
|
||||
$highlightActions = content[cursor[0]]?.actions ?? []
|
||||
}
|
||||
|
||||
function keypress(event: KeyboardEvent) {
|
||||
cursor++
|
||||
input.push(event.key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keypress={keypress} />
|
||||
|
||||
<div>
|
||||
<section>
|
||||
<!-- <div class="cursor" style="translate: calc({cursor}ch - 50%) -50%" /> -->
|
||||
{#each content as word, i}
|
||||
{#if word}
|
||||
{#each word.phrase as letter, j}
|
||||
<span>{KEYMAP_CODES[letter].id}</span>
|
||||
{/each}
|
||||
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<LayoutCC1 />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
section {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.letter {
|
||||
position: relative;
|
||||
filter: brightness(50%);
|
||||
}
|
||||
|
||||
.cursor {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
translate: -50% -50%;
|
||||
|
||||
width: 2px;
|
||||
height: 1em;
|
||||
|
||||
background: var(--md-sys-color-primary);
|
||||
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
</style>
|
||||
@@ -1,167 +1,302 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
||||
import charaActions from "$lib/assets/keymaps/chara-chorder.yml"
|
||||
import mouseActions from "$lib/assets/keymaps/mouse.yml"
|
||||
import keyboardActions from "$lib/assets/keymaps/keyboard.yml"
|
||||
import asciiActions from "$lib/assets/keymaps/ascii.yml"
|
||||
import cp1252Actions from "$lib/assets/keymaps/cp-1252.yml"
|
||||
import FlexSearch from "flexsearch"
|
||||
import {KEYMAP_CATEGORIES, KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||
import Index from "flexsearch"
|
||||
import {createEventDispatcher} from "svelte"
|
||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||
import LL from "../../../i18n/i18n-svelte"
|
||||
import {action} from "$lib/title"
|
||||
|
||||
const index = new FlexSearch({tokenize: "full"})
|
||||
export let currentAction: number | undefined = undefined
|
||||
export let nextAction: number | undefined = undefined
|
||||
|
||||
for (const code in KEYMAP_CODES) {
|
||||
const key = KEYMAP_CODES[code]
|
||||
index.add(
|
||||
code,
|
||||
`${key.id || key.code} ${key.title || ""} ${key.variant || ""} ${key.description || ""}`.trim(),
|
||||
const index = new Index({tokenize: "full"})
|
||||
for (const action of Object.values(KEYMAP_CODES)) {
|
||||
index?.add(
|
||||
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() {
|
||||
const query = searchInput.value
|
||||
customValue = query && !Number.isNaN(Number(query)) ? Number(query) : undefined
|
||||
results = query ? index.search(searchInput.value) : defaultActions
|
||||
results = index!.search(searchBox.value)
|
||||
exact = exactIndex[searchBox.value]?.code
|
||||
code = Number(searchBox.value)
|
||||
}
|
||||
|
||||
let customValue = undefined
|
||||
const defaultActions: string[] = [
|
||||
charaActions,
|
||||
mouseActions,
|
||||
keyboardActions,
|
||||
asciiActions,
|
||||
cp1252Actions,
|
||||
].flatMap(it => Object.keys(it.actions))
|
||||
let results: string[] = defaultActions
|
||||
let searchInput: HTMLInputElement
|
||||
function select(id?: number) {
|
||||
if (id !== undefined) {
|
||||
dispatch("select", id)
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardNavigation(event: KeyboardEvent) {
|
||||
if (event.shiftKey && event.key === "Enter") {
|
||||
dispatch("select", exact)
|
||||
} else if (event.key === "ArrowDown") {
|
||||
const element =
|
||||
resultList.querySelector("li:focus-within")?.nextSibling ?? resultList.querySelector("li:not(.exact)")
|
||||
if (element instanceof HTMLLIElement) {
|
||||
element.querySelector("button")?.focus()
|
||||
}
|
||||
} else if (event.key === "ArrowUp") {
|
||||
const element =
|
||||
resultList.querySelector("li:focus-within")?.previousSibling ??
|
||||
resultList.querySelector("li:not(.exact)")
|
||||
if (element instanceof HTMLLIElement) {
|
||||
element.querySelector("button")?.focus()
|
||||
}
|
||||
} else {
|
||||
searchBox.focus()
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
let results: number[] = Object.keys(KEYMAP_CODES).map(Number)
|
||||
let exact: number | undefined = undefined
|
||||
let code: number = Number.NaN
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let searchBox: HTMLInputElement
|
||||
let resultList: HTMLUListElement
|
||||
let filter: Set<number>
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<input type="search" on:input={search} placeholder="Search Actions" bind:this={searchInput} />
|
||||
<svelte:window on:keydown={keyboardNavigation} />
|
||||
|
||||
<div class="results">
|
||||
{#if customValue !== undefined}
|
||||
<button class="custom">
|
||||
Custom ActionID
|
||||
<span class="key">0x{customValue.toString(16).toUpperCase()}</span>
|
||||
</button>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
||||
<dialog open on:click|self={() => dispatch("close")}>
|
||||
<div class="content">
|
||||
<div class="search-row">
|
||||
<input
|
||||
type="search"
|
||||
bind:this={searchBox}
|
||||
on:input={search}
|
||||
on:keypress={event => {
|
||||
if (event.key === "Enter") {
|
||||
select(exact)
|
||||
}
|
||||
}}
|
||||
placeholder={$LL.actionSearch.PLACEHOLDER()}
|
||||
/>
|
||||
<button on:click={() => select(0)} use:action={{shortcut: "shift+esc"}}
|
||||
>{$LL.actionSearch.DELETE()}</button
|
||||
>
|
||||
<button
|
||||
use:action={{title: $LL.modal.CLOSE(), shortcut: "esc"}}
|
||||
class="icon"
|
||||
on:click={() => dispatch("close")}>close</button
|
||||
>
|
||||
</div>
|
||||
<fieldset class="filters">
|
||||
<label
|
||||
>{$LL.actionSearch.filter.ALL()}<input
|
||||
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}
|
||||
|
||||
{#each results as id}
|
||||
{@const key = KEYMAP_CODES[id]}
|
||||
<button title={key.description}>
|
||||
<div class="title">
|
||||
<b>
|
||||
{key.title || ""}
|
||||
{#if key.variant === "left"}
|
||||
(Left)
|
||||
{:else if key.variant === "right"}
|
||||
(Right)
|
||||
{/if}
|
||||
</b>
|
||||
{#if key.description}
|
||||
<i>{key.description}</i>
|
||||
{/if}
|
||||
</div>
|
||||
<span class:icon={!!key.icon} class="key">{key.icon || key.id || key.code}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<ul bind:this={resultList}>
|
||||
{#if exact !== undefined}
|
||||
<li class="exact">
|
||||
<i>Exact match</i>
|
||||
<ActionListItem id={exact} on:click={() => select(exact)} />
|
||||
</li>
|
||||
{/if}
|
||||
{#if !exact && code}
|
||||
{#if code >= 2 ** 5 && code < 2 ** 13}
|
||||
<li><button on:click={() => select(code)}>USE CODE</button></li>
|
||||
{:else}
|
||||
<li>Action code is out of range</li>
|
||||
{/if}
|
||||
{/if}
|
||||
{#each filter ? results.filter(it => filter.has(it)) : results as id (id)}
|
||||
<li><ActionListItem {id} on:click={() => select(id)} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</dialog>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
width: calc(min(100vw - 10px, 512px));
|
||||
height: calc(min(90vh, 600px));
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding-inline: 16px;
|
||||
|
||||
font-family: "Noto Sans Mono", monospace;
|
||||
font-size: 18px;
|
||||
color: var(--md-sys-color-on-primary);
|
||||
|
||||
background: var(--md-sys-color-primary);
|
||||
gap: 4px;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
|
||||
&::placeholder {
|
||||
color: inherit;
|
||||
opacity: 0.3;
|
||||
}
|
||||
label {
|
||||
height: unset;
|
||||
padding-block: 2px;
|
||||
padding-inline: 4px;
|
||||
|
||||
&::after {
|
||||
content: "plus";
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key {
|
||||
overflow: hidden;
|
||||
dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
|
||||
font-size: 18px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
|
||||
text-align: start;
|
||||
|
||||
> b {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-family: "Noto Sans Mono", monospace;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
|
||||
background: transparent;
|
||||
background: rgba(0 0 0 / 60%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.custom {
|
||||
padding: 8px;
|
||||
padding-inline-start: 16px;
|
||||
aside {
|
||||
pointer-events: none;
|
||||
|
||||
margin: 8px;
|
||||
|
||||
opacity: 0.4;
|
||||
border: 1px dashed var(--md-sys-color-outline);
|
||||
border-radius: 8px;
|
||||
|
||||
> h3 {
|
||||
width: fit-content;
|
||||
margin-block-start: -13px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 16px;
|
||||
padding-inline: 8px;
|
||||
|
||||
background: var(--md-sys-color-background);
|
||||
}
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
margin-inline: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
transform-origin: top left;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: calc(min(30cm, 90%));
|
||||
height: calc(min(100% - 128px, 90%));
|
||||
|
||||
color: var(--md-sys-color-on-background);
|
||||
|
||||
background: var(--md-sys-color-background);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.results {
|
||||
overflow-y: scroll;
|
||||
input[type="search"] {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin-block-end: 8px;
|
||||
padding-inline: 16px;
|
||||
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
color: currentcolor;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 1px solid var(--md-sys-color-primary);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
--scrollbar-color: var(--md-sys-color-surface-variant);
|
||||
|
||||
scrollbar-gutter: both-edges stable;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
li {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.exact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-block-start: 8px;
|
||||
|
||||
border: 1px solid var(--md-sys-color-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
> i {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding-inline: 6px;
|
||||
|
||||
color: var(--md-sys-color-on-primary);
|
||||
|
||||
background: var(--md-sys-color-primary);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
206
src/lib/components/layout/GenericLayout.svelte
Normal file
206
src/lib/components/layout/GenericLayout.svelte
Normal file
@@ -0,0 +1,206 @@
|
||||
<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"
|
||||
|
||||
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]
|
||||
const clickedGroup = groupParent.children.item(index) as SVGGElement
|
||||
const nextAction = get(layout)[get(activeLayer)][keyInfo.id]
|
||||
const currentAction = get(deviceLayout)[get(activeLayer)][keyInfo.id]
|
||||
const component = new ActionSelector({
|
||||
target: document.body,
|
||||
props: {
|
||||
currentAction,
|
||||
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}
|
||||
>
|
||||
{#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>
|
||||
83
src/lib/components/layout/KeyText.svelte
Normal file
83
src/lib/components/layout/KeyText.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<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[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 : inactiveOpacity}
|
||||
style:scale={isActive ? 1 : 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;
|
||||
}
|
||||
|
||||
text:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
116
src/lib/components/layout/KeyboardKey.svelte
Normal file
116
src/lib/components/layout/KeyboardKey.svelte
Normal file
@@ -0,0 +1,116 @@
|
||||
<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,44 +1,58 @@
|
||||
<script lang="ts">
|
||||
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"
|
||||
|
||||
$: device = $serialPort?.device ?? "ONE"
|
||||
let activeLayer = 0
|
||||
const activeLayer = getContext<Writable<number>>("active-layer")
|
||||
|
||||
const layers = [
|
||||
["Numeric Layer", "123", 1],
|
||||
["Primary Layer", "abc", 0],
|
||||
["Function Layer", "function", 2],
|
||||
] 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>
|
||||
|
||||
<div>
|
||||
<select bind:value={device}>
|
||||
<option value="ONE">CC1</option>
|
||||
<option value="LITE">Lite</option>
|
||||
</select>
|
||||
|
||||
<div class="container">
|
||||
<fieldset>
|
||||
{#each layers as [title, icon, value]}
|
||||
<button
|
||||
{title}
|
||||
class="icon"
|
||||
on:click={() => (activeLayer = value)}
|
||||
class:active={activeLayer === value}
|
||||
use:action={{title, shortcut: `alt+${value + 1}`}}
|
||||
on:click={() => ($activeLayer = value)}
|
||||
class:active={$activeLayer === value}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
{/each}
|
||||
</fieldset>
|
||||
|
||||
{#if device === "ONE"}
|
||||
<LayoutCC1 bind:activeLayer />
|
||||
{:else}
|
||||
<p>Unsupported device ({$serialPort?.device})</p>
|
||||
{/if}
|
||||
{#await layouts[device]() then visualLayout}
|
||||
<GenericLayout {visualLayout} />
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: 96px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
position: relative;
|
||||
|
||||
@@ -46,7 +60,6 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-block-end: -36px;
|
||||
padding: 0;
|
||||
|
||||
border: none;
|
||||
@@ -76,13 +89,19 @@
|
||||
outline: 8px solid var(--md-sys-color-background);
|
||||
}
|
||||
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
aspect-ratio: unset;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-inline-end: 16px;
|
||||
padding-inline: 4px 16px;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-inline-start: 16px;
|
||||
padding-inline: 16px 4px;
|
||||
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}} type="tertiary" />
|
||||
<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}} type="secondary" />
|
||||
</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}} type="secondary" />
|
||||
</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}} type="secondary" />
|
||||
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} type="secondary" />
|
||||
</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,181 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {highlightActions, layout} 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>
|
||||
export let type: "primary" | "secondary" | "tertiary" = "primary"
|
||||
|
||||
const layerNames = ["Primary Layer", "Number Layer", "Function Layer"]
|
||||
|
||||
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): KeyInfo[] {
|
||||
return Array.from({length: 3}).map((_, i) => {
|
||||
const actionId = layout?.[i][id]
|
||||
return KEYMAP_CODES[actionId]
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="radial {type}">
|
||||
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
||||
{@const actions = getActions(id, $layout)}
|
||||
<button
|
||||
use:editableLayout={{id, quadrant}}
|
||||
class:active={actions.some(it => it && $highlightActions?.includes(it.code))}
|
||||
>
|
||||
{#each actions as keyInfo, layer}
|
||||
{#if keyInfo}
|
||||
<span
|
||||
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
||||
class:icon={!!keyInfo.icon}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary > button {
|
||||
filter: brightness(80%) contrast(120%);
|
||||
}
|
||||
|
||||
.tertiary > button {
|
||||
filter: brightness(80%) contrast(110%);
|
||||
}
|
||||
</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
|
||||
}
|
||||
35
src/lib/dialogs/ConfirmDialog.svelte
Normal file
35
src/lib/dialogs/ConfirmDialog.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {createEventDispatcher} from "svelte"
|
||||
import Dialog from "$lib/dialogs/Dialog.svelte"
|
||||
|
||||
export let title: string
|
||||
export let message: string | undefined
|
||||
export let abortTitle: string
|
||||
export let confirmTitle: string
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Dialog>
|
||||
<h1>{@html title}</h1>
|
||||
{#if message}
|
||||
<p>{@html message}</p>
|
||||
{/if}
|
||||
<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>
|
||||
161
src/lib/dialogs/PickChangesDialog.svelte
Normal file
161
src/lib/dialogs/PickChangesDialog.svelte
Normal file
@@ -0,0 +1,161 @@
|
||||
<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
|
||||
.map((it, i) => /** @type {const} */ ([it, i + 1]))
|
||||
.filter(([it]) => it.length > 0) as [changes, layer]}
|
||||
<li>
|
||||
<h4>
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox" />
|
||||
{$LL.changes.layout.LAYER({changes: changes.length, layer})}
|
||||
</label>
|
||||
</h4>
|
||||
</li>
|
||||
{/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>
|
||||
31
src/lib/dialogs/confirm-dialog.ts
Normal file
31
src/lib/dialogs/confirm-dialog.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte"
|
||||
|
||||
export async function askForConfirmation(
|
||||
title: string,
|
||||
message: string,
|
||||
confirmTitle: string,
|
||||
abortTitle: string,
|
||||
): Promise<boolean> {
|
||||
const dialog = new ConfirmDialog({
|
||||
target: document.body,
|
||||
props: {
|
||||
title,
|
||||
message,
|
||||
confirmTitle,
|
||||
abortTitle,
|
||||
},
|
||||
})
|
||||
|
||||
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,32 +0,0 @@
|
||||
import tippy from "tippy.js"
|
||||
import InputEdit from "$lib/components/layout/InputEdit.svelte"
|
||||
import type {Action} from "svelte/action"
|
||||
|
||||
export const editableLayout: Action<HTMLButtonElement, {id: number; quadrant: number}> = (
|
||||
node,
|
||||
{id, quadrant},
|
||||
) => {
|
||||
let component: InputEdit | undefined
|
||||
const edit = tippy(node, {
|
||||
interactive: true,
|
||||
appendTo: document.body,
|
||||
trigger: "click",
|
||||
placement: (["top", "right", "bottom", "left"] as const)[quadrant],
|
||||
onShow(instance) {
|
||||
component ??= new InputEdit({
|
||||
target: instance.popper.querySelector(".tippy-content")!,
|
||||
props: {id},
|
||||
})
|
||||
},
|
||||
onHidden() {
|
||||
component?.$destroy()
|
||||
component = undefined
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
edit.destroy()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
font-family: "Material Symbols Rounded";
|
||||
font-size: 24px;
|
||||
font-feature-settings: "liga";
|
||||
font-variation-settings: "FILL" var(--icon-fill, 0), "wght" var(--icon-weigth, 400),
|
||||
font-variation-settings:
|
||||
"FILL" var(--icon-fill, 0),
|
||||
"wght" var(--icon-weigth, 400),
|
||||
"GRAD" var(--icon-grade, 0);
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
25
src/lib/os-layout.ts
Normal file
25
src/lib/os-layout.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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,5 +1,5 @@
|
||||
import type {Action} from "svelte/action"
|
||||
import {persistentWritable} from "$lib/storage"
|
||||
import type { Action } from "svelte/action"
|
||||
import { persistentWritable } from "$lib/storage"
|
||||
|
||||
export interface UserPreferences {
|
||||
backup: boolean
|
||||
@@ -13,7 +13,7 @@ export const theme = persistentWritable("user-theme", {
|
||||
|
||||
export const userPreferences = persistentWritable<UserPreferences>("user-preferences", {
|
||||
backup: false,
|
||||
autoConnect: true,
|
||||
autoConnect: false,
|
||||
})
|
||||
|
||||
export const preference: Action<HTMLInputElement, keyof UserPreferences> = (node, key) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {Writable} from "svelte/store"
|
||||
import type {CharaLayout} from "$lib/serialization/layout"
|
||||
import {persistentWritable} from "$lib/storage"
|
||||
import {userPreferences} from "$lib/preferences"
|
||||
import settingInfo from "$lib/assets/settings.yml"
|
||||
|
||||
export const serialPort = writable<CharaDevice | undefined>()
|
||||
|
||||
@@ -15,41 +16,86 @@ export interface 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",
|
||||
[[], [], []],
|
||||
() => get(userPreferences).backup,
|
||||
)
|
||||
|
||||
export const settings = writable({})
|
||||
|
||||
export const unsavedChanges = writable(0)
|
||||
|
||||
export const highlightActions: Writable<number[]> = writable([])
|
||||
/**
|
||||
* Settings as read from the device
|
||||
*/
|
||||
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 async function initSerial(manual = false) {
|
||||
const device = get(serialPort) ?? new CharaDevice()
|
||||
await device.init(manual)
|
||||
serialPort.set(device)
|
||||
sync()
|
||||
}
|
||||
|
||||
export async function sync() {
|
||||
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 i = 0; i < 90; i++) {
|
||||
for (let i = 0; i < device.keyCount; i++) {
|
||||
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i)
|
||||
progressTick()
|
||||
}
|
||||
}
|
||||
layout.set(parsedLayout)
|
||||
deviceLayout.set(parsedLayout)
|
||||
|
||||
const chordCount = await device.getChordCount()
|
||||
const chordInfo = []
|
||||
for (let i = 0; i < chordCount; i++) {
|
||||
chordInfo.push(await device.getChord(i))
|
||||
progressTick()
|
||||
}
|
||||
chords.set(chordInfo)
|
||||
deviceChords.set(chordInfo)
|
||||
syncStatus.set("done")
|
||||
syncProgress.set(undefined)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
import {LineBreakTransformer} from "$lib/serial/line-break-transformer"
|
||||
import {serialLog} from "$lib/serial/connection"
|
||||
import type {Chord} from "$lib/serial/chord"
|
||||
import {SemVer} from "$lib/serial/sem-ver"
|
||||
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}],
|
||||
])
|
||||
|
||||
const KEY_COUNTS = {
|
||||
ONE: 90,
|
||||
LITE: 67,
|
||||
X: 256,
|
||||
} as const
|
||||
|
||||
if (browser && navigator.serial === undefined && import.meta.env.TAURI_FAMILY !== undefined) {
|
||||
await import("./tauri-serial")
|
||||
}
|
||||
|
||||
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
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function canAutoConnect() {
|
||||
@@ -29,31 +51,66 @@ export class CharaDevice {
|
||||
|
||||
private lock?: Promise<true>
|
||||
|
||||
version!: [number, number, number]
|
||||
private readonly suspendDebounce = 100
|
||||
private suspendDebounceId?: number
|
||||
|
||||
version!: SemVer
|
||||
company!: "CHARACHORDER"
|
||||
device!: "ONE" | "LITE"
|
||||
device!: "ONE" | "LITE" | "X"
|
||||
chipset!: "M0" | "S2"
|
||||
keyCount!: 90 | 67 | 256
|
||||
|
||||
get portInfo() {
|
||||
return this.port.getInfo()
|
||||
}
|
||||
|
||||
constructor(private readonly baudRate = 115200) {}
|
||||
|
||||
async init(manual = false) {
|
||||
const ports = await getViablePorts()
|
||||
this.port =
|
||||
!manual && ports.length === 1
|
||||
? ports[0]
|
||||
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
|
||||
await this.port.open({baudRate: this.baudRate})
|
||||
const info = this.port.getInfo()
|
||||
serialLog.update(it => {
|
||||
it.push({
|
||||
type: "system",
|
||||
value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString(
|
||||
16,
|
||||
)}`,
|
||||
})
|
||||
return it
|
||||
})
|
||||
try {
|
||||
const ports = await getViablePorts()
|
||||
this.port =
|
||||
!manual && ports.length === 1
|
||||
? ports[0]
|
||||
: await navigator.serial.requestPort({filters: [...PORT_FILTERS.values()]})
|
||||
|
||||
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("VERSION").then(([version]) => version))
|
||||
const [company, device, chipset] = await this.send("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()
|
||||
}
|
||||
|
||||
private async wake() {
|
||||
await this.port.open({baudRate: this.baudRate})
|
||||
const decoderStream = new TextDecoderStream()
|
||||
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
|
||||
signal: this.abortController1.signal,
|
||||
@@ -64,13 +121,6 @@ export class CharaDevice {
|
||||
signal: this.abortController2.signal,
|
||||
})
|
||||
.getReader()
|
||||
|
||||
const [version] = await this.send("VERSION")
|
||||
this.version = version.split(".").map(Number) as [number, number, number]
|
||||
const [company, device, chipset] = await this.send("ID")
|
||||
this.company = company as "CHARACHORDER"
|
||||
this.device = device as "ONE" | "LITE"
|
||||
this.chipset = chipset as "M0" | "S2"
|
||||
}
|
||||
|
||||
private async internalRead() {
|
||||
@@ -105,19 +155,9 @@ export class CharaDevice {
|
||||
}
|
||||
|
||||
async forget() {
|
||||
await this.disconnect()
|
||||
await this.port.forget()
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this.reader.cancel()
|
||||
await this.streamClosed.catch(() => {
|
||||
/** noop */
|
||||
})
|
||||
this.reader.releaseLock()
|
||||
await this.port.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Read/write to serial port
|
||||
*/
|
||||
@@ -132,9 +172,23 @@ export class CharaDevice {
|
||||
const exec = new Promise<T>(async resolve => {
|
||||
let result!: T
|
||||
try {
|
||||
if (this.suspendDebounceId) {
|
||||
clearTimeout(this.suspendDebounceId)
|
||||
} else {
|
||||
await this.wake()
|
||||
}
|
||||
result = await callback(send, read)
|
||||
} finally {
|
||||
this.lock = undefined
|
||||
delete this.lock
|
||||
this.suspendDebounceId = setTimeout(() => {
|
||||
// cannot be locked here as all the code until clearTimeout is sync
|
||||
console.assert(this.lock === undefined)
|
||||
this.lock = this.suspend().then(() => {
|
||||
delete this.lock
|
||||
delete this.suspendDebounceId
|
||||
return true
|
||||
})
|
||||
}, this.suspendDebounce) as any
|
||||
resolve(result)
|
||||
}
|
||||
})
|
||||
@@ -174,7 +228,7 @@ export class CharaDevice {
|
||||
*/
|
||||
async getChordPhrase(actions: number[]): Promise<number[] | undefined> {
|
||||
const [phrase] = await this.send(`CML C2 ${stringifyChordActions(actions)}`)
|
||||
return phrase === "0" ? undefined : parsePhrase(phrase)
|
||||
return phrase === "2" ? undefined : parsePhrase(phrase)
|
||||
}
|
||||
|
||||
async setChord(chord: Chord) {
|
||||
@@ -184,12 +238,13 @@ export class CharaDevice {
|
||||
stringifyChordActions(chord.actions),
|
||||
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)}`)
|
||||
if (status.at(-1) !== "0") throw new Error(`Failed with status ${status}`)
|
||||
console.log(status)
|
||||
if (status.at(-1) !== "2") throw new Error(`Failed with status ${status}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +254,8 @@ export class CharaDevice {
|
||||
* @param action the assigned action id
|
||||
*/
|
||||
async setLayoutKey(layer: number, id: number, action: number) {
|
||||
const [status] = await this.send(`VAR B3 A${layer} ${id} ${action}`)
|
||||
const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`)
|
||||
console.log(status)
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||
}
|
||||
|
||||
@@ -234,7 +290,7 @@ export class CharaDevice {
|
||||
* To permanently store the settings, you *must* call commit.
|
||||
*/
|
||||
async setSetting(id: number, value: number) {
|
||||
const [status] = await this.send(`VAR B2 ${id} ${value}`)
|
||||
const [status] = await this.send(`VAR B2 ${id.toString(16).toUpperCase()} ${value}`)
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||
}
|
||||
|
||||
@@ -242,8 +298,9 @@ export class CharaDevice {
|
||||
* Retrieves a setting from the device
|
||||
*/
|
||||
async getSetting(id: number): Promise<number> {
|
||||
const [value, status] = await this.send(`VAR B1 ${id}`)
|
||||
if (status !== "0") throw new Error(`Setting "${id}" doesn't exist (Status code ${status})`)
|
||||
const [value, status] = await this.send(`VAR B1 ${id.toString(16).toUpperCase()}`)
|
||||
if (status !== "0")
|
||||
throw new Error(`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`)
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
@@ -252,8 +309,6 @@ export class CharaDevice {
|
||||
*/
|
||||
async reboot() {
|
||||
await this.send("RST")
|
||||
await this.disconnect()
|
||||
// TODO: reconnect
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,8 +316,13 @@ export class CharaDevice {
|
||||
*/
|
||||
async bootloader() {
|
||||
await this.send("RST BOOTLOADER")
|
||||
await this.disconnect()
|
||||
// TODO: more...
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the device
|
||||
*/
|
||||
async reset(type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC") {
|
||||
await this.send(`RST ${type}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,17 +5,35 @@ export interface KeyInfo extends Partial<ActionInfo> {
|
||||
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 =>
|
||||
load().then(it => (it as any).default),
|
||||
),
|
||||
)) as KeymapCategory[]
|
||||
|
||||
export const KEYMAP_CODES: Record<number, KeyInfo> = Object.fromEntries(
|
||||
keymaps.flatMap(category =>
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
Object.entries(category.actions).map(([code, action]) => [
|
||||
Number(code),
|
||||
{...action, code: Number(code), category},
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
export const KEYMAP_KEYCODES: Map<string, number> = new Map(
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
Object.entries(category.actions).map(([code, action]) => [action.keyCode!, Number(code)] as const),
|
||||
).filter(([keyCode]) => keyCode !== undefined),
|
||||
)
|
||||
|
||||
export const KEYMAP_IDS: Map<string, KeyInfo> = new Map(
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
Object.entries(category.actions).map(
|
||||
([code, action]) => [action.id!, {...action, code: Number(code), category}] as const,
|
||||
),
|
||||
).filter(([id]) => id !== undefined),
|
||||
)
|
||||
|
||||
export const specialKeycodes = new Map([
|
||||
[" ", 32], // Space
|
||||
])
|
||||
|
||||
27
src/lib/serial/sem-ver.ts
Normal file
27
src/lib/serial/sem-ver.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export class SemVer {
|
||||
major: number
|
||||
minor: number
|
||||
patch: number
|
||||
preRelease?: string
|
||||
meta?: string
|
||||
|
||||
constructor(versionString: string) {
|
||||
const [, major, minor, patch, preRelease, meta] =
|
||||
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+))?$/.exec(
|
||||
versionString,
|
||||
)!
|
||||
this.major = Number.parseInt(major)
|
||||
this.minor = Number.parseInt(minor)
|
||||
this.patch = Number.parseInt(patch)
|
||||
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}` : "")
|
||||
)
|
||||
}
|
||||
}
|
||||
12
src/lib/serial/updater.ts
Normal file
12
src/lib/serial/updater.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export async function updateDevice(port: SerialPort) {
|
||||
await port.open({
|
||||
baudRate: 115200,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: "none",
|
||||
bufferSize: 255,;
|
||||
})
|
||||
|
||||
const writer = port.writable!.getWriter()
|
||||
const reader = port.readable!.getReader()
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export function decompressActions(raw: Uint8Array): number[] {
|
||||
const actions: number[] = []
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
let action = raw[i]
|
||||
if (action < 32) {
|
||||
if (action > 0 && action < 32) {
|
||||
action = (action << 8) | raw[++i]
|
||||
}
|
||||
actions.push(action)
|
||||
|
||||
@@ -13,17 +13,16 @@ export async function toBase64(blob: Blob): Promise<string> {
|
||||
.replace(/^data:application\/octet-stream;base64,/, "")
|
||||
.replaceAll("+", ".")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "-")}-`,
|
||||
.replaceAll("=", "-")}`,
|
||||
)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
export async function fromBase64(base64: string): Promise<Blob> {
|
||||
export async function fromBase64(base64: string, fetch = window.fetch): Promise<Blob> {
|
||||
return fetch(
|
||||
`data:application/octet-stream;base64,${base64
|
||||
.replace(/-$/, "")
|
||||
.replaceAll(".", "+")
|
||||
.replaceAll("_", "/")
|
||||
.replaceAll("-", "=")}`,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import {compressActions, decompressActions} from "./actions"
|
||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
||||
|
||||
export interface NewCharaLayout {
|
||||
charaLayoutVersion: 1
|
||||
device: "one" | "lite" | string
|
||||
/**
|
||||
* Layers A1-A3, with numeric action codes on each
|
||||
*/
|
||||
layers: [number[], number[], number[]]
|
||||
}
|
||||
|
||||
export type CharaLayout = [number[], number[], number[]]
|
||||
|
||||
/**
|
||||
|
||||
106
src/lib/serialization/visual-layout.ts
Normal file
106
src/lib/serialization/visual-layout.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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,34 +1,66 @@
|
||||
import type {Action} from "svelte/action"
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
import {changes, ChangeType, settings} from "$lib/undo-redo"
|
||||
|
||||
export const setting: Action<HTMLInputElement, {id: number; inverse?: number; scale?: number}> = function (
|
||||
node: HTMLInputElement,
|
||||
{id, inverse, scale},
|
||||
) {
|
||||
node.setAttribute("disabled", "")
|
||||
const type = node.getAttribute("type") as "number" | "checkbox"
|
||||
const min = node.hasAttribute("min") ? Number(node.getAttribute("min")) : undefined
|
||||
const max = node.hasAttribute("max") ? Number(node.getAttribute("max")) : undefined
|
||||
console.log(min, max, "|", id, "|", node.getAttribute("min"), node.getAttribute("max"))
|
||||
|
||||
const unsubscribe = serialPort.subscribe(async port => {
|
||||
if (port) {
|
||||
const type = node.getAttribute("type") as "number" | "checkbox"
|
||||
const unsubscribe = settings.subscribe(async settings => {
|
||||
if (id in settings) {
|
||||
const {value, isApplied} = settings[id]
|
||||
if (type === "number") {
|
||||
const value = Number(await port.getSetting(id).then(it => it.toString()))
|
||||
node.value = (
|
||||
inverse !== undefined ? inverse / value : scale !== undefined ? scale * value : value
|
||||
).toString()
|
||||
} else {
|
||||
node.checked = await port.getSetting(id).then(it => it !== 0)
|
||||
node.checked = value !== 0
|
||||
}
|
||||
if (isApplied) {
|
||||
node.classList.remove("pending-changes")
|
||||
} else {
|
||||
node.classList.add("pending-changes")
|
||||
}
|
||||
node.removeAttribute("disabled")
|
||||
} else {
|
||||
node.setAttribute("disabled", "")
|
||||
}
|
||||
})
|
||||
function listener() {}
|
||||
node.addEventListener("input", listener)
|
||||
|
||||
async function listener() {
|
||||
let value: number
|
||||
if (type === "number") {
|
||||
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("change", listener)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("input", listener)
|
||||
node.removeEventListener("change", listener)
|
||||
unsubscribe()
|
||||
},
|
||||
}
|
||||
|
||||
53
src/lib/share/action-array.spec.ts
Normal file
53
src/lib/share/action-array.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
55
src/lib/share/action-array.ts
Normal file
55
src/lib/share/action-array.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {compressActions, decompressActions} from "../serialization/actions"
|
||||
import {CHARA_FILE_TYPES} from "../share/share-url"
|
||||
|
||||
export type ActionArray = number[] | ActionArray[]
|
||||
export function serializeActionArray(array: ActionArray): Uint8Array {
|
||||
let out = new Uint8Array(5)
|
||||
const writer = new DataView(out.buffer)
|
||||
writer.setUint32(0, array.length)
|
||||
|
||||
if (array.length === 0) {
|
||||
return out
|
||||
} else if (typeof array[0] === "number") {
|
||||
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number"))
|
||||
const compressed = compressActions(array as number[])
|
||||
writer.setUint32(0, compressed.length)
|
||||
return concatUint8Arrays(out, compressed)
|
||||
} else if (Array.isArray(array[0])) {
|
||||
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array"))
|
||||
return concatUint8Arrays(out, ...(array as ActionArray[]).map(serializeActionArray))
|
||||
} else {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
export function deserializeActionArray(raw: Uint8Array, cursor = {pos: 0}): 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") {
|
||||
const decompressed = decompressActions(raw.slice(cursor.pos, cursor.pos + length))
|
||||
cursor.pos += length
|
||||
return decompressed
|
||||
} else if (type === "array") {
|
||||
const out = []
|
||||
for (let i = 0; i < length; i++) {
|
||||
out.push(deserializeActionArray(raw, cursor))
|
||||
}
|
||||
return out
|
||||
} else {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
|
||||
const out = new Uint8Array(arrays.reduce((a, b) => a + b.length, 0))
|
||||
let offset = 0
|
||||
for (const array of arrays) {
|
||||
out.set(array, offset)
|
||||
offset += array.length
|
||||
}
|
||||
return out
|
||||
}
|
||||
23
src/lib/share/chara-file.ts
Normal file
23
src/lib/share/chara-file.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface CharaFile<T extends string> {
|
||||
charaVersion: 1
|
||||
type: T
|
||||
}
|
||||
|
||||
export interface CharaLayoutFile extends CharaFile<"layout"> {
|
||||
device?: "ONE" | "LITE" | string
|
||||
layout: [number[], number[], number[]]
|
||||
}
|
||||
|
||||
export interface CharaChordFile extends CharaFile<"chords"> {
|
||||
chords: [number[], number[]][]
|
||||
}
|
||||
|
||||
export interface CharaSettingsFile extends CharaFile<"settings"> {
|
||||
settings: number[]
|
||||
}
|
||||
|
||||
export interface CharaBackupFile extends CharaFile<"backup"> {
|
||||
history: [CharaChordFile, CharaLayoutFile, CharaSettingsFile][]
|
||||
}
|
||||
|
||||
export type CharaFiles = CharaLayoutFile | CharaChordFile | CharaSettingsFile
|
||||
66
src/lib/share/share-url.ts
Normal file
66
src/lib/share/share-url.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type {CharaFile, CharaFiles} from "../share/chara-file"
|
||||
import type {ActionArray} from "../share/action-array"
|
||||
import {deserializeActionArray, serializeActionArray} from "../share/action-array"
|
||||
import {fromBase64, toBase64} from "../serialization/base64"
|
||||
|
||||
type CharaLayoutOrder = {
|
||||
[K in CharaFiles["type"]]: Array<
|
||||
[Exclude<keyof Extract<CharaFiles, {type: K}>, keyof CharaFile<any>>, (typeof CHARA_FILE_TYPES)[number]]
|
||||
>
|
||||
}
|
||||
|
||||
const keys: CharaLayoutOrder = {
|
||||
layout: [
|
||||
["layout", "array"],
|
||||
["device", "string"],
|
||||
],
|
||||
chords: [["chords", "array"]],
|
||||
settings: [["settings", "array"]],
|
||||
}
|
||||
|
||||
export const CHARA_FILE_TYPES = ["unknown", "number", "string", "array"] as const
|
||||
|
||||
const sep = "\n"
|
||||
|
||||
export async function charaFileToUriComponent<T extends CharaFiles>(file: T): Promise<string> {
|
||||
let url = `${file.type}${sep}${file.charaVersion}`
|
||||
|
||||
for (const [key, type] of keys[file.type]) {
|
||||
const value = file[key as keyof T]
|
||||
url += sep
|
||||
if (type === "string") {
|
||||
url += value as string
|
||||
} else if (type === "array") {
|
||||
const stream = new Blob([serializeActionArray(value as ActionArray)])
|
||||
.stream()
|
||||
.pipeThrough(new CompressionStream("deflate"))
|
||||
url += await toBase64(await new Response(stream).blob())
|
||||
} else {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export async function charaFileFromUriComponent<T extends CharaFiles>(
|
||||
uriComponent: string,
|
||||
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]) {
|
||||
const value = values.shift()!
|
||||
if (type === "string") {
|
||||
file[key] = value
|
||||
} else if (type === "array") {
|
||||
const stream = (await fromBase64(value, fetch)).stream().pipeThrough(new DecompressionStream("deflate"))
|
||||
const actions = new Uint8Array(await new Response(stream).arrayBuffer())
|
||||
console.log(actions)
|
||||
file[key] = deserializeActionArray(actions)
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/lib/style/form/_button.scss
Normal file
75
src/lib/style/form/_button.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
color: currentcolor;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
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%;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: var(--md-sys-color-on-primary);
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
&.compact {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled,
|
||||
:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
16
src/lib/style/print.scss
Normal file
16
src/lib/style/print.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@media print {
|
||||
.print {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
--md-sys-color-background: white !important;
|
||||
--md-sys-color-on-background: black !important;
|
||||
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
10
src/lib/style/theme.scss
Normal file
10
src/lib/style/theme.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
@import "./form/button";
|
||||
@import "./form/toggle";
|
||||
@import "./form/checkbox";
|
||||
@import "./kbd";
|
||||
@import "./print";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
appearance: none;
|
||||
}
|
||||
@@ -24,6 +24,13 @@ $padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="tooltip"] {
|
||||
color: var(--md-sys-color-on-background);
|
||||
background-color: var(--md-sys-color-background);
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="search-completion"] {
|
||||
overflow: hidden;
|
||||
filter: none;
|
||||
|
||||
45
src/lib/title.ts
Normal file
45
src/lib/title.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type {Action} from "svelte/action"
|
||||
import tippy from "tippy.js"
|
||||
import type {SvelteComponent} from "svelte"
|
||||
import Tooltip from "$lib/components/Tooltip.svelte"
|
||||
import hotkeys from "hotkeys-js"
|
||||
|
||||
export const action: Action<Element, {title?: string; shortcut?: string}> = (
|
||||
node: Element,
|
||||
{title, shortcut},
|
||||
) => {
|
||||
let component: SvelteComponent | undefined
|
||||
const tooltip = tippy(node, {
|
||||
arrow: false,
|
||||
theme: "tooltip",
|
||||
animation: "fade",
|
||||
onShow(instance) {
|
||||
component ??= new Tooltip({
|
||||
target: instance.popper.querySelector(".tippy-content") as Element,
|
||||
props: {title, shortcut},
|
||||
})
|
||||
},
|
||||
onHidden() {
|
||||
component?.$destroy()
|
||||
component = undefined
|
||||
},
|
||||
})
|
||||
|
||||
if (shortcut && node instanceof HTMLElement) {
|
||||
hotkeys(shortcut, function (keyboardEvent) {
|
||||
keyboardEvent.preventDefault()
|
||||
node.click()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
update(updated) {
|
||||
title = updated.title
|
||||
shortcut = updated.shortcut
|
||||
},
|
||||
destroy() {
|
||||
tooltip.destroy()
|
||||
hotkeys.unbind(shortcut)
|
||||
},
|
||||
}
|
||||
}
|
||||
147
src/lib/undo-redo.ts
Normal file
147
src/lib/undo-redo.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import {persistentWritable} from "$lib/storage"
|
||||
import {derived} from "svelte/store"
|
||||
import type {Chord} from "$lib/serial/chord"
|
||||
import {deviceChords, deviceLayout, deviceSettings} from "$lib/serial/connection"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
|
||||
export enum ChangeType {
|
||||
Layout,
|
||||
Chord,
|
||||
Setting,
|
||||
}
|
||||
|
||||
export interface LayoutChange {
|
||||
type: ChangeType.Layout
|
||||
id: number
|
||||
layer: number
|
||||
action: number
|
||||
}
|
||||
|
||||
export interface ChordChange {
|
||||
type: ChangeType.Chord
|
||||
deleted?: true
|
||||
id: number[]
|
||||
actions: number[]
|
||||
phrase: number[]
|
||||
}
|
||||
|
||||
export interface SettingChange {
|
||||
type: ChangeType.Setting
|
||||
id: number
|
||||
setting: number
|
||||
}
|
||||
|
||||
export interface ChangeInfo {
|
||||
isApplied: boolean
|
||||
isCommitted?: boolean
|
||||
}
|
||||
|
||||
export type Change = LayoutChange | ChordChange | SettingChange
|
||||
|
||||
export const changes = persistentWritable<Change[]>("changes", [])
|
||||
|
||||
export interface Overlay {
|
||||
layout: [Map<number, number>, Map<number, number>, Map<number, number>]
|
||||
chords: Map<string, Chord & {deleted: boolean}>
|
||||
settings: Map<number, number>
|
||||
}
|
||||
|
||||
export const overlay = derived(changes, changes => {
|
||||
const overlay: Overlay = {
|
||||
layout: [new Map(), new Map(), new Map()],
|
||||
chords: new Map(),
|
||||
settings: new Map(),
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
switch (change.type) {
|
||||
case ChangeType.Layout:
|
||||
overlay.layout[change.layer].set(change.id, change.action)
|
||||
break
|
||||
case ChangeType.Chord:
|
||||
overlay.chords.set(JSON.stringify(change.id), {
|
||||
actions: change.actions,
|
||||
phrase: change.phrase,
|
||||
deleted: change.deleted ?? false,
|
||||
})
|
||||
break
|
||||
case ChangeType.Setting:
|
||||
overlay.settings.set(change.id, change.setting)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return overlay
|
||||
})
|
||||
|
||||
export const settings = derived([overlay, deviceSettings], ([overlay, settings]) =>
|
||||
settings.map<{value: number} & ChangeInfo>((value, id) => ({
|
||||
value: overlay.settings.get(id) ?? value,
|
||||
isApplied: !overlay.settings.has(id),
|
||||
})),
|
||||
)
|
||||
|
||||
export type KeyInfo = {action: number} & ChangeInfo
|
||||
export const layout = derived([overlay, deviceLayout], ([overlay, layout]) =>
|
||||
layout.map(
|
||||
(actions, layer) =>
|
||||
actions.map<KeyInfo>((action, id) => ({
|
||||
action: overlay.layout[layer].get(id) ?? action,
|
||||
isApplied: !overlay.layout[layer].has(id),
|
||||
})) as [KeyInfo, KeyInfo, KeyInfo],
|
||||
),
|
||||
)
|
||||
|
||||
export type ChordInfo = Chord &
|
||||
ChangeInfo & {phraseChanged: boolean; actionsChanged: boolean; sortBy: string} & {
|
||||
id: number[]
|
||||
deleted: boolean
|
||||
}
|
||||
export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
|
||||
const newChords = new Set(overlay.chords.keys())
|
||||
|
||||
const changedChords = chords.map<ChordInfo>(chord => {
|
||||
const id = JSON.stringify(chord.actions)
|
||||
if (overlay.chords.has(id)) {
|
||||
newChords.delete(id)
|
||||
const changedChord = overlay.chords.get(id)!
|
||||
return {
|
||||
id: chord.actions,
|
||||
// use the old phrase for stable editing
|
||||
sortBy: chord.phrase.map(it => KEYMAP_CODES[it]?.id ?? it).join(),
|
||||
actions: changedChord.actions,
|
||||
phrase: changedChord.phrase,
|
||||
actionsChanged: id !== JSON.stringify(changedChord.actions),
|
||||
phraseChanged: JSON.stringify(chord.phrase) !== JSON.stringify(changedChord.phrase),
|
||||
isApplied: false,
|
||||
deleted: changedChord.deleted,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: chord.actions,
|
||||
sortBy: chord.phrase.map(it => KEYMAP_CODES[it]?.id ?? it).join(),
|
||||
actions: chord.actions,
|
||||
phrase: chord.phrase,
|
||||
phraseChanged: false,
|
||||
actionsChanged: false,
|
||||
isApplied: true,
|
||||
deleted: false,
|
||||
}
|
||||
}
|
||||
})
|
||||
for (const id of newChords) {
|
||||
const chord = overlay.chords.get(id)!
|
||||
changedChords.push({
|
||||
sortBy: "",
|
||||
isApplied: false,
|
||||
actionsChanged: true,
|
||||
phraseChanged: false,
|
||||
deleted: chord.deleted,
|
||||
id: JSON.parse(id),
|
||||
phrase: chord.phrase,
|
||||
actions: chord.actions,
|
||||
})
|
||||
}
|
||||
|
||||
return changedChords.sort(({sortBy: a}, {sortBy: b}) => a.localeCompare(b))
|
||||
})
|
||||
1
src/lib/versioning.ts
Normal file
1
src/lib/versioning.ts
Normal file
@@ -0,0 +1 @@
|
||||
// TODO
|
||||
@@ -3,29 +3,36 @@
|
||||
import "$lib/fonts/material-symbols-rounded.scss"
|
||||
import "$lib/style/scrollbar.scss"
|
||||
import "$lib/style/tippy.scss"
|
||||
import "$lib/style/toggle.scss"
|
||||
import {onMount} from "svelte"
|
||||
import "$lib/style/theme.scss"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import {applyTheme, argbFromHex, themeFromSourceColor} from "@material/material-color-utilities"
|
||||
import Navigation from "./Navigation.svelte"
|
||||
import {canAutoConnect} from "$lib/serial/device"
|
||||
import {initSerial} from "$lib/serial/connection"
|
||||
import type {LayoutServerData} from "./$types"
|
||||
import type {LayoutData} from "./$types"
|
||||
import {browser} from "$app/environment"
|
||||
import BrowserWarning from "./BrowserWarning.svelte"
|
||||
import "tippy.js/animations/shift-away.css"
|
||||
import "tippy.js/dist/tippy.css"
|
||||
import tippy from "tippy.js"
|
||||
import {theme, userPreferences} from "$lib/preferences.js"
|
||||
import {setLocale} from "../i18n/i18n-svelte"
|
||||
import {LL, setLocale} from "../i18n/i18n-svelte"
|
||||
import {loadLocale} from "../i18n/i18n-util.sync"
|
||||
import {detectLocale} from "../i18n/i18n-util"
|
||||
import type {Locales} from "../i18n/i18n-types"
|
||||
import Footer from "./Footer.svelte"
|
||||
import {runLayoutDetection} from "$lib/os-layout.js"
|
||||
import PageTransition from "./PageTransition.svelte"
|
||||
import {restoreFromFile} from "$lib/backup/backup"
|
||||
import {goto} from "$app/navigation"
|
||||
|
||||
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
|
||||
loadLocale(locale)
|
||||
setLocale(locale)
|
||||
let stopLayoutDetection: () => void
|
||||
|
||||
if (browser) {
|
||||
stopLayoutDetection = runLayoutDetection()
|
||||
tippy.setDefaultProps({
|
||||
animation: "shift-away",
|
||||
theme: "surface-variant",
|
||||
@@ -36,7 +43,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
export let data: LayoutServerData
|
||||
export let data: LayoutData
|
||||
|
||||
onMount(async () => {
|
||||
theme.subscribe(it => {
|
||||
@@ -46,10 +53,22 @@
|
||||
})
|
||||
if (import.meta.env.TAURI_FAMILY === undefined) {
|
||||
const {initPwa} = await import("./pwa-setup")
|
||||
await initPwa()
|
||||
webManifestLink = await initPwa()
|
||||
}
|
||||
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) await initSerial()
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
||||
await initSerial()
|
||||
}
|
||||
if (data.importFile) {
|
||||
restoreFromFile(data.importFile)
|
||||
const url = new URL(location.href)
|
||||
url.searchParams.delete("import")
|
||||
await goto(url.href, {replaceState: true})
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
stopLayoutDetection?.()
|
||||
})
|
||||
|
||||
let webManifestLink = ""
|
||||
@@ -57,50 +76,26 @@
|
||||
|
||||
<svelte:head>
|
||||
{@html webManifestLink}
|
||||
<title>amaCC1ng</title>
|
||||
<meta name="description" content="Tool for CharaChorder devices" />
|
||||
<title>{$LL.TITLE()}</title>
|
||||
<meta name="description" content={$LL.DESCRIPTION()} />
|
||||
<meta name="theme-color" content={data.themeColor} />
|
||||
</svelte:head>
|
||||
|
||||
<Navigation />
|
||||
|
||||
<main>
|
||||
<!-- <PickChangesDialog /> -->
|
||||
|
||||
<PageTransition>
|
||||
<slot />
|
||||
</main>
|
||||
</PageTransition>
|
||||
|
||||
<Footer />
|
||||
|
||||
{#if import.meta.env.TAURI_FAMILY === undefined && browser && !("serial" in navigator)}
|
||||
<BrowserWarning />
|
||||
{/if}
|
||||
|
||||
<style lang="scss" global>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--md-sys-color-tertiary);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@@ -122,8 +117,7 @@
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
|
||||
padding: 16px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
import type {LayoutLoad} from "./$types"
|
||||
import {browser} from "$app/environment"
|
||||
import {charaFileFromUriComponent} from "$lib/share/share-url"
|
||||
|
||||
export const prerender = true
|
||||
export const trailingSlash = "always"
|
||||
|
||||
export const load = (async ({url, data, fetch}) => {
|
||||
const importFile = browser && new URLSearchParams(url.search).get("import")
|
||||
return {
|
||||
...data,
|
||||
importFile: importFile ? await charaFileFromUriComponent(importFile, fetch) : undefined,
|
||||
}
|
||||
}) satisfies LayoutLoad
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>dot i/o</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>dot i/o V2</h1>
|
||||
|
||||
<section>
|
||||
<h2>Layout</h2>
|
||||
</section>
|
||||
@@ -1,45 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {parseCompressed, stringifyCompressed} from "$lib/serial/serialization"
|
||||
import {chords, layout} from "$lib/serial/connection"
|
||||
import {preference} from "$lib/preferences"
|
||||
import type {Chord} from "$lib/serial/chord"
|
||||
import type {CharaLayout} from "$lib/serialization/layout"
|
||||
import LL from "../i18n/i18n-svelte"
|
||||
|
||||
interface Backup {
|
||||
isCharaBackup: string
|
||||
chords: Chord[]
|
||||
layout: CharaLayout
|
||||
}
|
||||
|
||||
async function downloadBackup() {
|
||||
const downloadUrl = URL.createObjectURL(
|
||||
await stringifyCompressed({
|
||||
isCharaBackup: "v1.0",
|
||||
chords: $chords,
|
||||
layout: $layout,
|
||||
}),
|
||||
)
|
||||
const element = document.createElement("a")
|
||||
element.setAttribute("download", "chords.chb")
|
||||
element.href = downloadUrl
|
||||
element.setAttribute("target", "_blank")
|
||||
element.click()
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
}
|
||||
|
||||
async function restoreBackup(event: Event) {
|
||||
const input = (event.target as HTMLInputElement).files![0]
|
||||
if (!input) return
|
||||
const backup = await parseCompressed<Backup>(input)
|
||||
if (backup.isCharaBackup !== "v1.0") throw new Error("Invalid Backup")
|
||||
if (backup.chords) {
|
||||
$chords = backup.chords
|
||||
}
|
||||
if (backup.layout) {
|
||||
$layout = backup.layout
|
||||
}
|
||||
}
|
||||
import {
|
||||
createChordBackup,
|
||||
createLayoutBackup,
|
||||
createSettingsBackup,
|
||||
downloadBackup,
|
||||
downloadFile,
|
||||
restoreBackup,
|
||||
} from "$lib/backup/backup"
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@@ -47,9 +16,24 @@
|
||||
<p class="disclaimer">
|
||||
<i>{$LL.backup.DISCLAIMER()}</i>
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>{$LL.backup.INDIVIDUAL()}</legend>
|
||||
<button on:click={() => downloadFile(createChordBackup())}>
|
||||
<span class="icon">piano</span>
|
||||
{$LL.configure.chords.TITLE()}
|
||||
</button>
|
||||
<button on:click={() => downloadFile(createLayoutBackup())}>
|
||||
<span class="icon">keyboard</span>
|
||||
{$LL.configure.layout.TITLE()}
|
||||
</button>
|
||||
<button on:click={() => downloadFile(createSettingsBackup())}>
|
||||
<span class="icon">settings</span>
|
||||
{$LL.configure.settings.TITLE()}
|
||||
</button>
|
||||
</fieldset>
|
||||
<div class="save">
|
||||
<button class="primary" on:click={downloadBackup}
|
||||
><span class="icon">save</span>{$LL.backup.DOWNLOAD()}</button
|
||||
><span class="icon">download</span>{$LL.backup.DOWNLOAD()}</button
|
||||
>
|
||||
<label class="button"
|
||||
><input on:input={restoreBackup} type="file" /><span class="icon">settings_backup_restore</span
|
||||
@@ -72,6 +56,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
margin-block: 16px;
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -95,34 +86,4 @@
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.button,
|
||||
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: "Noto Sans Mono", monospace;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-background);
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: var(--md-sys-color-on-primary);
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
>{$LL.browserWarning.INFO_BROWSER_SUFFIX()}
|
||||
</p>
|
||||
<div>
|
||||
<a href="https://github.com/Theaninova/dotio/releases" target="_blank"
|
||||
>{$LL.browserWarning.DOWNLOAD_APP()}</a
|
||||
>
|
||||
<p>{$LL.browserWarning.DOWNLOAD_APP()}</p>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -50,24 +48,14 @@
|
||||
|
||||
a {
|
||||
color: var(--md-sys-color-on-error);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div > a {
|
||||
div > p {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
display: inline-block;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
background: var(--md-sys-color-on-error);
|
||||
}
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user