Compare commits
310 Commits
v0.2.0
...
a3bf9ac32b
| Author | SHA1 | Date | |
|---|---|---|---|
|
a3bf9ac32b
|
|||
|
|
5bd3245084 | ||
|
1cd2ec318a
|
|||
|
6c8bfa0272
|
|||
|
f69be14b5e
|
|||
|
dce554fc66
|
|||
|
f152dbdcf5
|
|||
|
6a29e6a2fc
|
|||
|
9bf3801fef
|
|||
|
d2accfb838
|
|||
|
b8a376b93b
|
|||
|
588719df91
|
|||
|
6a0dad9dad
|
|||
|
f3704e4051
|
|||
|
3e6298717e
|
|||
|
aced0bbbb7
|
|||
|
|
36874c59e3 | ||
|
9dc61a3482
|
|||
|
d9183f952a
|
|||
|
913a833824
|
|||
|
0d6ef4d011
|
|||
|
232045964c
|
|||
|
3659b80e41
|
|||
|
3a02caeb6d
|
|||
|
259fd3a989
|
|||
|
dcf1d89fa0
|
|||
|
c79237ce22
|
|||
|
d68f1b19fa
|
|||
|
9cb36662b3
|
|||
|
b4605fe84d
|
|||
|
06d122b5d6
|
|||
|
3d25b030c6
|
|||
|
bf490ba823
|
|||
|
397f4bb6a9
|
|||
|
1f4604bcbc
|
|||
|
68faf57a22
|
|||
|
1d976947e1
|
|||
|
ca8bfac3bc
|
|||
|
2f0d8f2e1d
|
|||
|
236e23086c
|
|||
|
d1fefb88a1
|
|||
|
26c43b1966
|
|||
|
8b2bfee099
|
|||
|
b8b903c5e1
|
|||
|
6201cf5b0c
|
|||
|
aaafadf732
|
|||
|
fe80867ce4
|
|||
|
72a8e084ce
|
|||
|
989e844190
|
|||
|
500221f39a
|
|||
|
|
d91273d27b | ||
|
888df6dd66
|
|||
|
7ad9612037
|
|||
|
3f9674b399
|
|||
|
92ba5bcb24
|
|||
|
2163a63a7c
|
|||
|
65a5a2517e
|
|||
|
21e8c291b0
|
|||
|
4106a80d53
|
|||
|
|
01fb61d27c | ||
|
3dd91a1cea
|
|||
|
cbcf705f71
|
|||
|
4007810c7b
|
|||
|
f322435c41
|
|||
|
587375e654
|
|||
|
0500a723de
|
|||
|
26dcc56aca
|
|||
|
20b65813bf
|
|||
|
87b23c04b1
|
|||
|
8b2bc6d109
|
|||
|
19cf0b26b3
|
|||
|
3e72dd3cb8
|
|||
|
a40daefbad
|
|||
|
77d4a90519
|
|||
|
c9a031a1fd
|
|||
|
254a0c1aec
|
|||
|
bd75012cf1
|
|||
|
4b738bb340
|
|||
|
3af65106bf
|
|||
|
8087d10d5a
|
|||
|
2782966505
|
|||
|
5b6d369101
|
|||
|
b423d1c661
|
|||
|
92a3c6012f
|
|||
|
8ec11c7ec9
|
|||
|
5c8eb1d19f
|
|||
|
91a044bbba
|
|||
|
1a6c85a361
|
|||
|
ecef11ac2d
|
|||
|
a23af9ba9d
|
|||
|
93849f250f
|
|||
|
33890b0aa8
|
|||
|
6f925de1af
|
|||
|
d45fe43f17
|
|||
|
59788f059d
|
|||
|
2808973ad0
|
|||
|
bef51d2a7d
|
|||
|
854ab6d3be
|
|||
|
86ec8651b6
|
|||
|
4e4bff02a0
|
|||
|
5d4dbc7e2a
|
|||
|
dfd1c0bcbd
|
|||
|
6ac2cd1993
|
|||
|
7256dc50d4
|
|||
|
f0ad19e6c2
|
|||
|
9022a09b4c
|
|||
|
7e3e61afd7
|
|||
|
08f594d164
|
|||
|
046595b51f
|
|||
|
fbc5303690
|
|||
|
ad41d39bfb
|
|||
|
6faaa18b3b
|
|||
|
6ab6959129
|
|||
|
44d89d3f35
|
|||
|
eaf0adaf01
|
|||
|
5b6a5ea36d
|
|||
|
14cbb5553b
|
|||
|
|
8ed72fe958 | ||
|
06b83f79ef
|
|||
|
5fa4b1fd09
|
|||
|
f585a0ebda
|
|||
|
a48e2b5a16
|
|||
|
fd612eda1d
|
|||
|
a1fe6f7110
|
|||
|
0e57e810e0
|
|||
|
a15d5dde38
|
|||
|
560206129e
|
|||
|
cb7c70dac1
|
|||
|
edabf8ec84
|
|||
|
f2f61f32f2
|
|||
|
a3857843d6
|
|||
|
c1b1068c4b
|
|||
|
2411dd2bea
|
|||
|
7911904906
|
|||
|
630687de80
|
|||
|
84b22e0006
|
|||
|
dd070c8856
|
|||
|
6872cd0554
|
|||
|
628007af23
|
|||
|
19fad84357
|
|||
|
f172318a78
|
|||
|
c2e3850082
|
|||
|
7a5a4eb434
|
|||
|
c878311f62
|
|||
|
fb3fb246e9
|
|||
|
b4e4ca84a4
|
|||
|
c1b1544256
|
|||
|
03dd528465
|
|||
|
81af9f2e82
|
|||
|
6bb42429e5
|
|||
|
d07751a944
|
|||
|
8867030ede
|
|||
|
faaa6dd5be
|
|||
|
43cf13094e
|
|||
|
ed523628ff
|
|||
|
98b451eec9
|
|||
|
6e37dc198f
|
|||
|
e319b1bfaf
|
|||
|
eb33b64100
|
|||
|
766bc44a85
|
|||
|
b679aa377a
|
|||
|
ea3192d4e6
|
|||
|
256daec412
|
|||
|
29a07133d1
|
|||
|
c3bd8431e5
|
|||
|
c8e04ed6cc
|
|||
|
d98653995b
|
|||
|
3dd9611ebf
|
|||
|
9d9360375b
|
|||
|
d683c8c70c
|
|||
|
d8d430f333
|
|||
|
fe850f47ec
|
|||
|
f9a63a8724
|
|||
|
af01426f43
|
|||
|
9d7cefb3b4
|
|||
|
f44e5a79de
|
|||
|
8b2e92c124
|
|||
|
f758be91a9
|
|||
|
bf4c86e698
|
|||
|
50a09d2008
|
|||
|
3c1a4de4a7
|
|||
|
8cbdf1393f
|
|||
|
1ccb17f053
|
|||
|
532dc70fe2
|
|||
|
d5893013f9
|
|||
|
80308cad73
|
|||
|
2d59bd016f
|
|||
|
298de49257
|
|||
|
3a62864a41
|
|||
|
109095e35e
|
|||
|
2dd6f39ac6
|
|||
|
b0f653e73b
|
|||
|
d552fb9220
|
|||
|
77339620e6
|
|||
|
846183bbb1
|
|||
|
1d53f6df7a
|
|||
|
58d13a4107
|
|||
|
f7d99d8d7b
|
|||
|
d9dd003b01
|
|||
|
dc798d2b9f
|
|||
|
c2ec460c8c
|
|||
|
c51bcc8ff0
|
|||
|
63b7f8ab18
|
|||
|
eaf8028538
|
|||
|
2ad0ef3b6d
|
|||
|
20705de069
|
|||
|
64b519d5b1
|
|||
|
fb490b3db6
|
|||
|
c37ae7da7b
|
|||
|
5c06c2206c
|
|||
|
f9cdf70bdb
|
|||
|
3a6483aa61
|
|||
|
|
018c7a5eac | ||
|
f73b8c1453
|
|||
|
e38d952e1d
|
|||
|
8e5692ca59
|
|||
|
a0fe925ea9
|
|||
|
e84470d577
|
|||
|
683561dc06
|
|||
|
2fd2dad6f7
|
|||
|
e2f9f87b13
|
|||
|
623d895aea
|
|||
|
561300de64
|
|||
|
c5d9defc9d
|
|||
|
acd58646f6
|
|||
|
3634264af3
|
|||
|
3515994a5a
|
|||
|
bdebe238ae
|
|||
|
ebf7d73d20
|
|||
|
e19a57efac
|
|||
|
034436f93e
|
|||
|
2710f7fc25
|
|||
|
d2276a53d0
|
|||
|
8701d7a40d
|
|||
|
94cfaf40e5
|
|||
|
c661a4b30b
|
|||
|
9b95e1d67a
|
|||
|
f7bf93fcfc
|
|||
|
08df049170
|
|||
|
65a536cdea
|
|||
|
d2fd84a6b5
|
|||
|
88429412b9
|
|||
|
ef309d603e
|
|||
|
fade2f978e
|
|||
|
a1760d518c
|
|||
|
9d33565081
|
|||
|
|
11fe12f095 | ||
|
aba390839b
|
|||
|
|
a6e7df55ff | ||
|
|
7e5e7b8f5f | ||
|
|
a34ba35889 | ||
|
|
616d15b6bd | ||
|
|
283444f0be | ||
|
|
e5e56c04a2 | ||
|
|
a34c176bcc | ||
|
e4d51cd51d
|
|||
|
a7b49de6ac
|
|||
|
fc86b31337
|
|||
|
d8f0679233
|
|||
|
c93246ee8c
|
|||
|
22905c2b56
|
|||
|
074f1da48d
|
|||
|
e7a52221d2
|
|||
|
f03b4d586b
|
|||
|
4cd9ce536d
|
|||
|
a39f57bac1
|
|||
|
bf96c1e29d
|
|||
|
a134b970ee
|
|||
|
86476cfdd8
|
|||
|
742e7a6b98
|
|||
|
607338878b
|
|||
|
777488ecd1
|
|||
|
220c8cbe67
|
|||
|
42922e7ce0
|
|||
|
9c1918e683
|
|||
|
5014e1e8e8
|
|||
|
e0f5c6440c
|
|||
|
e21ff12804
|
|||
|
2fa8d93d60
|
|||
|
aa1d4787f5
|
|||
|
4cc9462655
|
|||
|
7d148d0c2c
|
|||
|
73c71836dc
|
|||
|
e508d1312e
|
|||
|
c709878d6a
|
|||
| 374e27c7d0 | |||
| 88c7f057c9 | |||
| 6b09cbfbec | |||
| 06c1121983 | |||
| 2130b6c7b9 | |||
| e64082d578 | |||
| 21dbfa48de | |||
| 7df75e109d | |||
| 5cdf969c6d | |||
| 634073f10d | |||
| 4cc3343984 | |||
| 998a400395 | |||
| c0fb737314 | |||
| c59b2732f7 | |||
| 9bf1a13e02 | |||
| 6facaad4a2 | |||
| b04ed7fe7f | |||
| 4eb1e8c049 | |||
| 26ca9984ea | |||
| 110771a2a4 | |||
| 7fdf1cd3b4 | |||
| c4fee59446 | |||
| 088aa0dbcf | |||
| 26a6f70ccb | |||
| 391c9d8837 |
95
.github/workflows/build.yml
vendored
@@ -1,65 +1,50 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on: [push]
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "master" ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: 🔨 Build
|
name: 🔨🚀 Build and deploy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 🚚 Checkout
|
- name: 🚚 Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 🐍 Use Python 3.x
|
- name: 🐍 Use Python 3.x
|
||||||
uses: actions/setup-python@v3.1.4
|
uses: actions/setup-python@v3.1.4
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
cache: pip
|
cache: pip
|
||||||
- name: ⏬ Install Python dependencies
|
- name: ⏬ Install Python dependencies
|
||||||
run: python -m venv venv
|
run: pip install -r requirements.txt
|
||||||
- run: ./venv/bin/pip install -r requirements.txt
|
|
||||||
|
|
||||||
- name: 🐉 Use Node.js 18.16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18.16.x
|
|
||||||
cache: 'npm'
|
|
||||||
- name: ⏬ Install Node dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: 🔥 Optimize icon font
|
- name: Install pnpm
|
||||||
run: npm run minify-icons
|
uses: pnpm/action-setup@v4
|
||||||
- name: 🔨 Build site
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: 📦 Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v3.1.2
|
|
||||||
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:
|
with:
|
||||||
name: build
|
version: 9
|
||||||
path: build
|
- name: 🐉 Use Node.js 22.4.x
|
||||||
- name: 🚀 Deploy
|
uses: actions/setup-node@v3
|
||||||
uses: SamKirkland/web-deploy@v1
|
|
||||||
with:
|
with:
|
||||||
target-server: ${{ secrets.SSH_SERVER }}
|
node-version: 22.4.x
|
||||||
destination-path: ~/public_html/
|
cache: "pnpm"
|
||||||
source-path: ./build/
|
- name: ⏬ Install Node dependencies
|
||||||
remote-user: ${{ secrets.SSH_USER }}
|
run: pnpm install
|
||||||
private-ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
ssh-port: ${{ secrets.SSH_PORT }}
|
- name: 🔥 Optimize icon font
|
||||||
|
run: pnpm minify-icons
|
||||||
|
- name: 🔨 Build site
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
run: |
|
||||||
|
install -m 600 -D /dev/null ~/.ssh/id_rsa
|
||||||
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||||
|
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Publish Stable
|
||||||
|
if: ${{ github.ref == 'refs/tags/v*' }}
|
||||||
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
|
||||||
|
|
||||||
|
- name: Publish Branch
|
||||||
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${GITHUB_REF##*/}
|
||||||
|
- name: Publish Commit
|
||||||
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${{ github.sha }}
|
||||||
|
|||||||
54
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: "publish desktop apps"
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "desktop-app-v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-tauri:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [macos-latest, ubuntu-20.04, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: 🚚 Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 🐍 Use Python 3.x
|
||||||
|
uses: actions/setup-python@v3.1.4
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
cache: pip
|
||||||
|
- name: ⏬ Install Python dependencies
|
||||||
|
run: pip install -r requirements.txt
|
||||||
|
- name: 🐉 Use Node.js 18.16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.16.x
|
||||||
|
cache: "npm"
|
||||||
|
- name: 🦀 Use Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: 🐧 Install Linux Dependencies
|
||||||
|
if: matrix.platform == 'ubuntu-20.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libudev-dev libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: ⏬ Install Node dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: 🔥 Optimize icon font
|
||||||
|
run: npm run minify-icons
|
||||||
|
- name: 📦 Build, Package & Release
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
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."
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
1
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules
|
|||||||
/package
|
/package
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
.direnv
|
||||||
!.env.example
|
!.env.example
|
||||||
venv
|
venv
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
/src-tauri/target
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Ignore files for PNPM, NPM and YARN
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
|
static/languages/*.json
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
{
|
{
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
"pluginSearchDirs": ["."],
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
.typesafe-i18n.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||||
|
"baseLocale": "en",
|
||||||
|
"adapter": "svelte"
|
||||||
|
}
|
||||||
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.
|
||||||
33
README.md
@@ -1,25 +1,12 @@
|
|||||||
# dot i/o V2
|
# CharaChorder Device Manager
|
||||||
|
|
||||||

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

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

|
||||||
|

|
||||||
|
[](https://manager.charachorder.com/)
|
||||||
|
|
||||||
I aim to create a new site that offers an easier, visually pleasing
|
Get the latest desktop release [here](https://github.com/CharaChorder/DeviceManager/releases).
|
||||||
and more complete way to configure and learn CharaChorder devices.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
- NodeJS >=18.16
|
|
||||||
- Python >=3.10 virtual environment
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
@@ -42,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`
|
After that, add the `SSH_SERVER`, `SSH_PORT`, `SSH_PRIVATE_KEY` and `SSH_USER`
|
||||||
environment secrets to your environment in GitHub.
|
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
@@ -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.
|
||||||
96
flake.lock
generated
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1722415718,
|
||||||
|
"narHash": "sha256-5US0/pgxbMksF92k1+eOa8arJTJiPvsdZj9Dl+vJkM4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c3392ad349a5227f4a3464dce87bcc5046692fce",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1718428119,
|
||||||
|
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1722391647,
|
||||||
|
"narHash": "sha256-JTi7l1oxnatF1uX/gnGMlRnyFMtylRw4MqhCUdoN2K4=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "0fd4a5d2098faa516a9b83022aec7db766cd1de8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
78
flake.nix
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
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; [
|
||||||
|
nodejs_22
|
||||||
|
nodePackages.pnpm
|
||||||
|
rust-bin
|
||||||
|
fontMin
|
||||||
|
])
|
||||||
|
++ (with tauriPkgs; [
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
pkg-config
|
||||||
|
dbus
|
||||||
|
openssl_3
|
||||||
|
glib
|
||||||
|
gtk3
|
||||||
|
libsoup
|
||||||
|
webkitgtk
|
||||||
|
librsvg
|
||||||
|
# serial plugin
|
||||||
|
udev
|
||||||
|
]);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = packages;
|
||||||
|
shellHook = ''
|
||||||
|
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
140
icons.config.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/** @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: [
|
||||||
|
"deployed_code_update",
|
||||||
|
"adjust",
|
||||||
|
"add",
|
||||||
|
"piano",
|
||||||
|
"keyboard",
|
||||||
|
"settings",
|
||||||
|
"edit",
|
||||||
|
"music_note",
|
||||||
|
"avg_pace",
|
||||||
|
"lyrics",
|
||||||
|
"speed",
|
||||||
|
"cognition",
|
||||||
|
"update",
|
||||||
|
"offline_pin",
|
||||||
|
"warning",
|
||||||
|
"dangerous",
|
||||||
|
"check",
|
||||||
|
"cable",
|
||||||
|
"person",
|
||||||
|
"sync",
|
||||||
|
"school",
|
||||||
|
"restart_alt",
|
||||||
|
"usb",
|
||||||
|
"usb_off",
|
||||||
|
"rule_settings",
|
||||||
|
"123",
|
||||||
|
"abc",
|
||||||
|
"function",
|
||||||
|
"cloud_done",
|
||||||
|
"backup",
|
||||||
|
"cloud_download",
|
||||||
|
"cloud_off",
|
||||||
|
"share",
|
||||||
|
"ios_share",
|
||||||
|
"close",
|
||||||
|
"arrow_back",
|
||||||
|
"arrow_back_ios_new",
|
||||||
|
"save",
|
||||||
|
"settings_backup_restore",
|
||||||
|
"sort",
|
||||||
|
"shopping_bag",
|
||||||
|
"filter_list",
|
||||||
|
"settings_power",
|
||||||
|
"link",
|
||||||
|
"link_off",
|
||||||
|
"chevron_right",
|
||||||
|
"check_circle",
|
||||||
|
"error",
|
||||||
|
"auto_delete",
|
||||||
|
"format_paint",
|
||||||
|
"dark_mode",
|
||||||
|
"light_mode",
|
||||||
|
"palette",
|
||||||
|
"translate",
|
||||||
|
"play_arrow",
|
||||||
|
"extension",
|
||||||
|
"upload_file",
|
||||||
|
"commit",
|
||||||
|
"bug_report",
|
||||||
|
"delete",
|
||||||
|
"remove_selection",
|
||||||
|
"bolt",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
"replay",
|
||||||
|
"reply",
|
||||||
|
"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",
|
||||||
|
"send",
|
||||||
|
"more_horiz",
|
||||||
|
"add_reaction",
|
||||||
|
"stop",
|
||||||
|
"description",
|
||||||
|
"add_circle",
|
||||||
|
"refresh",
|
||||||
|
"tune",
|
||||||
|
"edit_document",
|
||||||
|
"chat",
|
||||||
|
"account_circle",
|
||||||
|
"experiment",
|
||||||
|
"code",
|
||||||
|
"dictionary",
|
||||||
|
"developer_board",
|
||||||
|
"developer_board_off",
|
||||||
|
"memory",
|
||||||
|
],
|
||||||
|
codePoints: {
|
||||||
|
speed: "e9e4",
|
||||||
|
arrow_split: "e985",
|
||||||
|
arrow_circle_down: "f181",
|
||||||
|
arrow_circle_up: "f182",
|
||||||
|
counter_1: "f784",
|
||||||
|
counter_2: "f783",
|
||||||
|
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",
|
||||||
|
routine: "e20c",
|
||||||
|
experiment: "e686",
|
||||||
|
dictionary: "f539",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
export interface IconsConfig {
|
|
||||||
codePoints: Record<string, string>
|
|
||||||
inputPath: string
|
|
||||||
outputPath: string
|
|
||||||
icons: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: IconsConfig = {
|
|
||||||
inputPath:
|
|
||||||
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
|
|
||||||
outputPath: "src/lib/assets/icons.min.woff2",
|
|
||||||
icons: [
|
|
||||||
"piano",
|
|
||||||
"keyboard",
|
|
||||||
"settings",
|
|
||||||
"music_note",
|
|
||||||
"avg_pace",
|
|
||||||
"lyrics",
|
|
||||||
"speed",
|
|
||||||
"cognition",
|
|
||||||
"update",
|
|
||||||
"offline_pin",
|
|
||||||
"warning",
|
|
||||||
"cable",
|
|
||||||
"person",
|
|
||||||
"sync",
|
|
||||||
"restart_alt",
|
|
||||||
"usb",
|
|
||||||
"rule_settings",
|
|
||||||
"123",
|
|
||||||
"abc",
|
|
||||||
"function",
|
|
||||||
"cloud_done",
|
|
||||||
"backup",
|
|
||||||
"cloud_download",
|
|
||||||
"share",
|
|
||||||
"ios_share",
|
|
||||||
"close",
|
|
||||||
"arrow_back",
|
|
||||||
"arrow_back_ios_new",
|
|
||||||
"save",
|
|
||||||
"settings_backup_restore",
|
|
||||||
],
|
|
||||||
codePoints: {
|
|
||||||
speed: "e9e4",
|
|
||||||
arrow_split: "e985",
|
|
||||||
arrow_circle_down: "f181",
|
|
||||||
arrow_circle_up: "f182",
|
|
||||||
counter_1: "f784",
|
|
||||||
counter_2: "f783",
|
|
||||||
counter_3: "f782",
|
|
||||||
ios_share: "e6b8",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
9030
package-lock.json
generated
128
package.json
@@ -1,53 +1,97 @@
|
|||||||
{
|
{
|
||||||
"name": "cccs",
|
"name": "charachorder-device-manager",
|
||||||
"version": "0.2.0",
|
"version": "2.2.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.4",
|
||||||
|
"pnpm": ">=9.4"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/CharaChorder/DeviceManager.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://docs.charachorder.com",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/CharaChorder/DeviceManager/issues"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "npm-run-all --parallel vite typesafe-i18n",
|
||||||
"build": "vite build",
|
"dev:external": "npm-run-all --parallel vite:external typesafe-i18n",
|
||||||
|
"dev:tauri": "tauri dev",
|
||||||
|
"vite": "vite dev",
|
||||||
|
"vite:external": "vite --host",
|
||||||
|
"build": "typesafe-i18n --no-watch && vite build",
|
||||||
|
"build:tauri": "tauri build",
|
||||||
|
"tauri": "tauri",
|
||||||
"test": "vitest run --coverage",
|
"test": "vitest run --coverage",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
"minify-icons": "node src/tools/minify-icon-font.js",
|
||||||
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
|
"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 .",
|
"lint": "prettier --check .",
|
||||||
"format": "prettier --plugin-search-dir . --write ."
|
"format": "prettier --write .",
|
||||||
|
"typesafe-i18n": "typesafe-i18n"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@theaninova/prettier-config": "^1.0.0",
|
"@codemirror/autocomplete": "^6.18.2",
|
||||||
"@types/w3c-web-serial": "^1.0.3",
|
"@codemirror/commands": "^6.7.1",
|
||||||
"@vite-pwa/sveltekit": "^0.2.5",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.4",
|
"@codemirror/language": "^6.10.3",
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.4",
|
"@codemirror/state": "^6.4.1",
|
||||||
"stylelint": "^15.9.0",
|
"@codemirror/view": "^6.34.1",
|
||||||
"stylelint-config-standard-scss": "^10.0.0",
|
"@fontsource-variable/material-symbols-rounded": "^5.1.3",
|
||||||
"stylelint-config-prettier-scss": "^1.0.0",
|
"@fontsource-variable/noto-sans-mono": "^5.1.0",
|
||||||
|
"@lezer/highlight": "^1.2.1",
|
||||||
|
"@material/material-color-utilities": "^0.3.0",
|
||||||
|
"@melt-ui/pp": "^0.3.2",
|
||||||
|
"@melt-ui/svelte": "^0.86.0",
|
||||||
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
|
"@sveltejs/kit": "^2.7.5",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"@tauri-apps/api": "^1.6.0",
|
||||||
|
"@tauri-apps/cli": "^1.6.0",
|
||||||
|
"@types/dom-view-transitions": "^1.0.5",
|
||||||
|
"@types/flexsearch": "^0.7.6",
|
||||||
|
"@types/w3c-web-serial": "^1.0.7",
|
||||||
|
"@types/w3c-web-usb": "^1.0.10",
|
||||||
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"codemirror": "^6.0.1",
|
||||||
|
"cypress": "^13.13.2",
|
||||||
|
"d3": "^7.9.0",
|
||||||
|
"esptool-js": "^0.4.7",
|
||||||
|
"flexsearch": "^0.7.43",
|
||||||
|
"fontkit": "^2.0.4",
|
||||||
|
"glob": "^11.0.0",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"matrix-js-sdk": "^34.9.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-svelte": "^3.2.7",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"sass": "^1.80.6",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"stylelint": "^16.10.0",
|
||||||
|
"stylelint-config-clean-order": "^6.1.0",
|
||||||
"stylelint-config-html": "^1.1.0",
|
"stylelint-config-html": "^1.1.0",
|
||||||
"stylelint-config-recommended-scss": "^12.0.0",
|
"stylelint-config-prettier-scss": "^1.0.0",
|
||||||
"stylelint-config-clean-order": "^5.0.1",
|
"stylelint-config-recommended-scss": "^14.1.0",
|
||||||
"glob": "^10.3.1",
|
"stylelint-config-standard-scss": "^13.1.0",
|
||||||
"flexsearch": "^0.7.31",
|
"svelte": "5.1.9",
|
||||||
"@sveltejs/adapter-static": "^2.0.2",
|
"svelte-check": "^4.0.5",
|
||||||
"@sveltejs/kit": "^1.20.4",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
"tippy.js": "^6.3.7",
|
||||||
"jsdom": "^22.1.0",
|
"typesafe-i18n": "^5.26.2",
|
||||||
"@material/material-color-utilities": "^0.2.7",
|
"typescript": "^5.6.3",
|
||||||
"fontkit": "^2.0.2",
|
"vite": "^5.4.10",
|
||||||
"prettier": "^2.8.0",
|
"vite-plugin-mkcert": "^1.17.6",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"svelte": "^4.0.0",
|
"vitest": "^2.1.4",
|
||||||
"svelte-check": "^3.4.3",
|
"web-serial-polyfill": "^1.0.15",
|
||||||
"ts-node": "^10.9.1",
|
"workbox-window": "^7.3.0"
|
||||||
"typescript": "^5.0.0",
|
|
||||||
"vitest": "^0.33.0",
|
|
||||||
"vite": "^4.3.6",
|
|
||||||
"vite-plugin-pwa": "^0.16.4",
|
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
|
||||||
"svelte-preprocess": "^5.0.4",
|
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"sass": "^1.63.6"
|
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module"
|
||||||
"prettier": "@theaninova/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
8581
pnpm-lock.yaml
generated
Normal file
3
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
4075
src-tauri/Cargo.lock
generated
Normal file
27
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "app"
|
||||||
|
version = "2.2.3"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
||||||
|
license = "AGPL-3"
|
||||||
|
repository = "https://github.com/Theaninova/dotio"
|
||||||
|
default-run = "app"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.60"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.4.0", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serialport = "4.2.1"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
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.
|
||||||
|
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||||
|
# DO NOT REMOVE!!
|
||||||
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
11
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod serial;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(serial::init())
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
99
src-tauri/src/serial.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use serialport::{available_ports, SerialPort, SerialPortType};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tauri::plugin::{Builder, TauriPlugin};
|
||||||
|
use tauri::{command, generate_handler, Manager, Runtime, State};
|
||||||
|
|
||||||
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
|
Builder::new("serial")
|
||||||
|
.invoke_handler(generate_handler![
|
||||||
|
get_serial_ports,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
read,
|
||||||
|
write
|
||||||
|
])
|
||||||
|
.setup(move |app_handle| {
|
||||||
|
app_handle.manage(SerialState::default());
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SerialState {
|
||||||
|
handles: Arc<Mutex<HashMap<String, Box<dyn SerialPort>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct WebSerialPortInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub usb_product_id: u16,
|
||||||
|
pub usb_vendor_id: u16,
|
||||||
|
pub serial_number: Option<String>,
|
||||||
|
pub manufacturer: Option<String>,
|
||||||
|
pub product: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
fn get_serial_ports() -> Result<Vec<WebSerialPortInfo>, String> {
|
||||||
|
Ok(available_ports()
|
||||||
|
.map_err(|err| err.to_string())?
|
||||||
|
.iter()
|
||||||
|
.filter_map(|port| match &port.port_type {
|
||||||
|
SerialPortType::UsbPort(usb) => Some(WebSerialPortInfo {
|
||||||
|
name: port.port_name.clone(),
|
||||||
|
usb_vendor_id: usb.vid,
|
||||||
|
usb_product_id: usb.pid,
|
||||||
|
serial_number: usb.serial_number.clone(),
|
||||||
|
manufacturer: usb.manufacturer.clone(),
|
||||||
|
product: usb.product.clone(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
fn open(state: State<'_, SerialState>, path: String, baud_rate: u32) -> Result<(), String> {
|
||||||
|
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
|
||||||
|
if handles.contains_key(&path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let port = serialport::new(path.clone(), baud_rate)
|
||||||
|
.open()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
handles.insert(path, port);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
fn close(state: State<'_, SerialState>, path: String) -> Result<(), String> {
|
||||||
|
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
|
||||||
|
handles.remove(&path).ok_or("Port is already closed")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
fn read(state: State<'_, SerialState>, path: String) -> Result<Vec<u8>, String> {
|
||||||
|
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
|
||||||
|
let port = handles.get_mut(&path).ok_or("Read: Port is not open")?;
|
||||||
|
|
||||||
|
let size = port.bytes_to_read().map_err(|err| err.to_string())?;
|
||||||
|
let mut buffer: Vec<u8> = vec![0; size as usize];
|
||||||
|
port.read_exact(buffer.as_mut_slice())
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
fn write(state: State<'_, SerialState>, path: String, chunk: Vec<u8>) -> Result<(), String> {
|
||||||
|
let mut handles = state.handles.lock().map_err(|err| err.to_string())?;
|
||||||
|
let port = handles.get_mut(&path).ok_or("Write: Port is not open")?;
|
||||||
|
port.write_all(&chunk).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
63
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"build": {
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devPath": "http://localhost:5173",
|
||||||
|
"distDir": "../build"
|
||||||
|
},
|
||||||
|
"package": { "productName": "amacc1ng", "version": "2.2.3" },
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": { "all": false },
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"copyright": "AGPL-3.0-or-later",
|
||||||
|
"deb": { "depends": [] },
|
||||||
|
"externalBin": [],
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"identifier": "de.theaninova.chara-app",
|
||||||
|
"longDescription": "",
|
||||||
|
"macOS": {
|
||||||
|
"entitlements": null,
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"frameworks": [],
|
||||||
|
"providerShortName": null,
|
||||||
|
"signingIdentity": null
|
||||||
|
},
|
||||||
|
"resources": [],
|
||||||
|
"shortDescription": "",
|
||||||
|
"targets": "all",
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": { "csp": null },
|
||||||
|
"updater": {
|
||||||
|
"active": true,
|
||||||
|
"endpoints": [
|
||||||
|
"https://amacc1ng.theaninova.de/update?current_version={{current_version}}&target={{target}}&arch={{arch}}",
|
||||||
|
"https://dotio.theaninova.de/update?current_version={{current_version}}&target={{target}}&arch={{arch}}"
|
||||||
|
],
|
||||||
|
"dialog": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU5QjEwMEY5RjNBRjM4MEIKUldRTE9LL3orUUN4V2FMWDZkc2l2VUdOL3FSdUMwTk1ualNac095RVZXVEpqUEtORkFsWGZaTmsK"
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"fullscreen": false,
|
||||||
|
"height": 720,
|
||||||
|
"resizable": true,
|
||||||
|
"title": "amacc1ng",
|
||||||
|
"width": 1280
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/app.d.ts
vendored
@@ -11,4 +11,4 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export {};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/icon.svg" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
23
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/// <references types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly TAURI_FAMILY?: string;
|
||||||
|
readonly TAURI_PLATFORM_VERSION?: string;
|
||||||
|
readonly TAURI_TARGET_TRIPLE?: string;
|
||||||
|
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 VITE_LEARN_URL: string;
|
||||||
|
readonly VITE_LATEST_FIRMWARE: string;
|
||||||
|
readonly VITE_STORE_URL: string;
|
||||||
|
readonly VITE_MATRIX_URL: string;
|
||||||
|
readonly VITE_FIRMWARE_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
1
src/i18n/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
i18n-*.ts
|
||||||
144
src/i18n/de/index.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import type { Translation } from "../i18n-types";
|
||||||
|
|
||||||
|
const de = {
|
||||||
|
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: "Backup",
|
||||||
|
AUTO_BACKUP: "Auto-backup",
|
||||||
|
DISCLAIMER:
|
||||||
|
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
|
||||||
|
DOWNLOAD: "Alles",
|
||||||
|
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",
|
||||||
|
theme: {
|
||||||
|
TITLE: "Darstellung",
|
||||||
|
COLOR_SCHEME: "Farbschema",
|
||||||
|
DARK_MODE: "Dunkel",
|
||||||
|
LIGHT_MODE: "Hell",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deviceManager: {
|
||||||
|
TITLE: "Gerät",
|
||||||
|
AUTO_CONNECT: "Automatisch Verbinden",
|
||||||
|
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. Flatpak und Snap Anwendungen benötigen zusätzliche Berechtigungen oder funktionieren möglicherweise gar nicht.",
|
||||||
|
bootMenu: {
|
||||||
|
TITLE: "Bootmenü",
|
||||||
|
REBOOT: "Neustarten",
|
||||||
|
BOOTLOADER: "Bootloader",
|
||||||
|
POWER_WARNING:
|
||||||
|
"Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
browserWarning: {
|
||||||
|
TITLE: "Warnung",
|
||||||
|
INFO_SERIAL_PREFIX:
|
||||||
|
"Der aktuell genutzte Browser wird aufgrund der speziellen Voraussetzung für Kommunikation über die ",
|
||||||
|
INFO_SERIAL_INFIX: "serielle Schnittstelle",
|
||||||
|
INFO_SERIAL_SUFFIX: " nicht unterstützt.",
|
||||||
|
INFO_BROWSER_PREFIX:
|
||||||
|
"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:
|
||||||
|
"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: "Bibliothek",
|
||||||
|
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 würde einen bereits existierenden Akkord überschreiben. Wirklich fortfahren?",
|
||||||
|
CONFIRM: "Überschreiben",
|
||||||
|
ABORT: "Überspringen",
|
||||||
|
},
|
||||||
|
VOCABULARY: "Vokabelliste",
|
||||||
|
TRY_TYPING: "Versuche hier zu tippen",
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
TITLE: "Layout",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
TITLE: "Gerät",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugin: {
|
||||||
|
editor: {
|
||||||
|
RUN: "Ausführen",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Translation;
|
||||||
|
|
||||||
|
export default de;
|
||||||
143
src/i18n/en/index.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import type { BaseTranslation } from "../i18n-types";
|
||||||
|
|
||||||
|
const en = {
|
||||||
|
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: "Backup",
|
||||||
|
AUTO_BACKUP: "Auto-backup",
|
||||||
|
DISCLAIMER:
|
||||||
|
"Whenever you connect this device to browser, a backup is made locally and kept only on your computer.",
|
||||||
|
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",
|
||||||
|
theme: {
|
||||||
|
TITLE: "Theme",
|
||||||
|
COLOR_SCHEME: "Color scheme",
|
||||||
|
DARK_MODE: "Dark",
|
||||||
|
LIGHT_MODE: "Light",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deviceManager: {
|
||||||
|
TITLE: "Device",
|
||||||
|
AUTO_CONNECT: "Auto-connect",
|
||||||
|
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. Flatpak or Snap versions in particular might need additional permissions or may not work at all.",
|
||||||
|
bootMenu: {
|
||||||
|
TITLE: "Boot Menu",
|
||||||
|
REBOOT: "Reboot",
|
||||||
|
BOOTLOADER: "Bootloader",
|
||||||
|
POWER_WARNING:
|
||||||
|
"To reboot from bootloader you need to physically reconnect your device.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
browserWarning: {
|
||||||
|
TITLE: "Warning",
|
||||||
|
INFO_SERIAL_PREFIX:
|
||||||
|
"Your current browser is not supported due to this site's unique requirement for ",
|
||||||
|
INFO_SERIAL_INFIX: "serial connections",
|
||||||
|
INFO_SERIAL_SUFFIX: ".",
|
||||||
|
INFO_BROWSER_PREFIX:
|
||||||
|
"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:
|
||||||
|
"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: "Library",
|
||||||
|
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 conflicts with an existing chord. Are you sure you want to overwrite this chord?",
|
||||||
|
CONFIRM: "Overwrite",
|
||||||
|
ABORT: "Skip",
|
||||||
|
},
|
||||||
|
VOCABULARY: "Vocabulary",
|
||||||
|
TRY_TYPING: "Try typing here",
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
TITLE: "Layout",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
TITLE: "Device",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugin: {
|
||||||
|
editor: {
|
||||||
|
RUN: "Run",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies BaseTranslation;
|
||||||
|
|
||||||
|
export default en;
|
||||||
12
src/i18n/formatters.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
return formatters;
|
||||||
|
};
|
||||||
55
src/lib/PageTransition.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import { afterNavigate, beforeNavigate } from "$app/navigation";
|
||||||
|
import { expoIn, expoOut } from "svelte/easing";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let { children, routeOrder }: { children: Snippet; routeOrder: string[]; direction: } =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
let inDirection = $state(0);
|
||||||
|
let outDirection = $state(0);
|
||||||
|
let outroEnd: undefined | (() => void) = $state(undefined);
|
||||||
|
let animationDone: Promise<void>;
|
||||||
|
|
||||||
|
let isNavigating = $state(false);
|
||||||
|
|
||||||
|
function routeIndex(route: string | undefined): number {
|
||||||
|
return routeOrder.findIndex((it) => route?.startsWith(it));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeNavigate((navigation) => {
|
||||||
|
const from = routeIndex(navigation.from?.url.pathname);
|
||||||
|
const to = routeIndex(navigation.to?.url.pathname);
|
||||||
|
if (from === -1 || to === -1 || from === to) return;
|
||||||
|
isNavigating = true;
|
||||||
|
|
||||||
|
inDirection = from > to ? -1 : 1;
|
||||||
|
outDirection = from > to ? 1 : -1;
|
||||||
|
|
||||||
|
animationDone = new Promise((resolve) => {
|
||||||
|
outroEnd = resolve;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterNavigate(async () => {
|
||||||
|
await animationDone;
|
||||||
|
isNavigating = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !isNavigating}
|
||||||
|
<main
|
||||||
|
in:fly={{ y: inDirection * 24, duration: 150, easing: expoOut }}
|
||||||
|
out:fly={{ y: outDirection * 24, duration: 150, easing: expoIn }}
|
||||||
|
onoutroend={outroEnd}
|
||||||
|
>
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
main {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
name: Action Codes
|
|
||||||
description: 10-bit action codes 0x00-0x1F
|
|
||||||
actions:
|
|
||||||
0x00:
|
|
||||||
id: "0x00"
|
|
||||||
0x01:
|
|
||||||
id: "0x01"
|
|
||||||
0x02:
|
|
||||||
id: "0x02"
|
|
||||||
0x03:
|
|
||||||
id: "0x03"
|
|
||||||
0x04:
|
|
||||||
id: "0x04"
|
|
||||||
0x05:
|
|
||||||
id: "0x05"
|
|
||||||
0x06:
|
|
||||||
id: "0x06"
|
|
||||||
0x07:
|
|
||||||
id: "0x07"
|
|
||||||
0x08:
|
|
||||||
id: "0x08"
|
|
||||||
0x09:
|
|
||||||
id: "0x09"
|
|
||||||
0x0A:
|
|
||||||
id: "0x0A"
|
|
||||||
0x0B:
|
|
||||||
id: "0x0B"
|
|
||||||
0x0C:
|
|
||||||
id: "0x0C"
|
|
||||||
0x0D:
|
|
||||||
id: "0x0D"
|
|
||||||
0x0E:
|
|
||||||
id: "0x0E"
|
|
||||||
0x0F:
|
|
||||||
id: "0x0F"
|
|
||||||
0x10:
|
|
||||||
id: "0x10"
|
|
||||||
0x11:
|
|
||||||
id: "0x11"
|
|
||||||
0x12:
|
|
||||||
id: "0x12"
|
|
||||||
0x13:
|
|
||||||
id: "0x13"
|
|
||||||
0x14:
|
|
||||||
id: "0x14"
|
|
||||||
0x15:
|
|
||||||
id: "0x15"
|
|
||||||
0x16:
|
|
||||||
id: "0x16"
|
|
||||||
0x17:
|
|
||||||
id: "0x17"
|
|
||||||
0x18:
|
|
||||||
id: "0x18"
|
|
||||||
0x19:
|
|
||||||
id: "0x19"
|
|
||||||
0x1A:
|
|
||||||
id: "0x1A"
|
|
||||||
0x1B:
|
|
||||||
id: "0x1B"
|
|
||||||
0x1C:
|
|
||||||
id: "0x1C"
|
|
||||||
0x1D:
|
|
||||||
id: "0x1D"
|
|
||||||
0x1E:
|
|
||||||
id: "0x1E"
|
|
||||||
0x1F:
|
|
||||||
id: "0x1F"
|
|
||||||
144
src/lib/assets/keymaps/ascii-macros.yml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
name: ASCII Macros
|
||||||
|
description: ASCII Characters that are macros for SHFT + key
|
||||||
|
actions:
|
||||||
|
33:
|
||||||
|
id: "!"
|
||||||
|
title: Exclamation Point
|
||||||
|
34:
|
||||||
|
id: '"'
|
||||||
|
title: Double Quote
|
||||||
|
35:
|
||||||
|
id: "#"
|
||||||
|
title: Hash Symbol
|
||||||
|
36:
|
||||||
|
id: "$"
|
||||||
|
title: Dollar Sign
|
||||||
|
37:
|
||||||
|
id: "%"
|
||||||
|
title: Percent
|
||||||
|
38:
|
||||||
|
id: "&"
|
||||||
|
title: Ampersand
|
||||||
|
40:
|
||||||
|
id: "("
|
||||||
|
title: Opening Parenthesis
|
||||||
|
41:
|
||||||
|
id: ")"
|
||||||
|
title: Closing Parenthesis
|
||||||
|
42:
|
||||||
|
id: "*"
|
||||||
|
title: Asterisk
|
||||||
|
43:
|
||||||
|
id: "+"
|
||||||
|
title: Plus
|
||||||
|
58:
|
||||||
|
id: ":"
|
||||||
|
title: Colon
|
||||||
|
60:
|
||||||
|
id: "<"
|
||||||
|
title: Less Than
|
||||||
|
62:
|
||||||
|
id: ">"
|
||||||
|
title: Greater Than
|
||||||
|
63:
|
||||||
|
id: "?"
|
||||||
|
title: Question Mark
|
||||||
|
64:
|
||||||
|
id: "@"
|
||||||
|
title: At Symbol
|
||||||
|
65:
|
||||||
|
id: "A"
|
||||||
|
title: Uppercase A
|
||||||
|
66:
|
||||||
|
id: "B"
|
||||||
|
title: Uppercase B
|
||||||
|
67:
|
||||||
|
id: "C"
|
||||||
|
title: Uppercase C
|
||||||
|
68:
|
||||||
|
id: "D"
|
||||||
|
title: Uppercase D
|
||||||
|
69:
|
||||||
|
id: "E"
|
||||||
|
title: Uppercase E
|
||||||
|
70:
|
||||||
|
id: "F"
|
||||||
|
title: Uppercase F
|
||||||
|
71:
|
||||||
|
id: "G"
|
||||||
|
title: Uppercase G
|
||||||
|
72:
|
||||||
|
id: "H"
|
||||||
|
title: Uppercase H
|
||||||
|
73:
|
||||||
|
id: "I"
|
||||||
|
title: Uppercase I
|
||||||
|
74:
|
||||||
|
id: "J"
|
||||||
|
title: Uppercase J
|
||||||
|
75:
|
||||||
|
id: "K"
|
||||||
|
title: Uppercase K
|
||||||
|
76:
|
||||||
|
id: "L"
|
||||||
|
title: Uppercase L
|
||||||
|
77:
|
||||||
|
id: "M"
|
||||||
|
title: Uppercase M
|
||||||
|
78:
|
||||||
|
id: "N"
|
||||||
|
title: Uppercase N
|
||||||
|
79:
|
||||||
|
id: "O"
|
||||||
|
title: Uppercase O
|
||||||
|
80:
|
||||||
|
id: "P"
|
||||||
|
title: Uppercase P
|
||||||
|
81:
|
||||||
|
id: "Q"
|
||||||
|
title: Uppercase Q
|
||||||
|
82:
|
||||||
|
id: "R"
|
||||||
|
title: Uppercase R
|
||||||
|
83:
|
||||||
|
id: "S"
|
||||||
|
title: Uppercase S
|
||||||
|
84:
|
||||||
|
id: "T"
|
||||||
|
title: Uppercase T
|
||||||
|
85:
|
||||||
|
id: "U"
|
||||||
|
title: Uppercase U
|
||||||
|
86:
|
||||||
|
id: "V"
|
||||||
|
title: Uppercase V
|
||||||
|
87:
|
||||||
|
id: "W"
|
||||||
|
title: Uppercase W
|
||||||
|
88:
|
||||||
|
id: "X"
|
||||||
|
title: Uppercase X
|
||||||
|
89:
|
||||||
|
id: "Y"
|
||||||
|
title: Uppercase Y
|
||||||
|
90:
|
||||||
|
id: "Z"
|
||||||
|
title: Uppercase Z
|
||||||
|
94:
|
||||||
|
id: "^"
|
||||||
|
title: Caret
|
||||||
|
95:
|
||||||
|
id: "_"
|
||||||
|
title: Underscore
|
||||||
|
123:
|
||||||
|
id: "{"
|
||||||
|
title: Left Curly Brace
|
||||||
|
124:
|
||||||
|
id: "|"
|
||||||
|
title: Pipe
|
||||||
|
125:
|
||||||
|
id: "}"
|
||||||
|
title: Right Curly Brace
|
||||||
|
126:
|
||||||
|
id: "~"
|
||||||
|
title: Tilde
|
||||||
@@ -7,289 +7,195 @@ actions:
|
|||||||
description: |
|
description: |
|
||||||
While SPACE is used for keymaps and chord, just a " " is used in chord outputs.
|
While SPACE is used for keymaps and chord, just a " " is used in chord outputs.
|
||||||
This action is unique in this way. Technically it is "printable", but it is not visible.
|
This action is unique in this way. Technically it is "printable", but it is not visible.
|
||||||
33:
|
|
||||||
id: "!"
|
|
||||||
title: Exclamation Point
|
|
||||||
34:
|
|
||||||
id: '"'
|
|
||||||
title: Double Quote
|
|
||||||
35:
|
|
||||||
id: "#"
|
|
||||||
title: Hash Symbol
|
|
||||||
36:
|
|
||||||
id: "$"
|
|
||||||
title: Dollar Sign
|
|
||||||
37:
|
|
||||||
id: "%"
|
|
||||||
title: Percent
|
|
||||||
38:
|
|
||||||
id: "&"
|
|
||||||
title: Ampersand
|
|
||||||
39:
|
39:
|
||||||
id: "'"
|
id: "'"
|
||||||
|
keyCode: Quote
|
||||||
title: Single Quote
|
title: Single Quote
|
||||||
40:
|
|
||||||
id: "("
|
|
||||||
title: Opening Parenthesis
|
|
||||||
41:
|
|
||||||
id: ")"
|
|
||||||
title: Closing Parenthesis
|
|
||||||
42:
|
|
||||||
id: "*"
|
|
||||||
title: Asterisk
|
|
||||||
43:
|
|
||||||
id: "+"
|
|
||||||
title: Plus
|
|
||||||
44:
|
44:
|
||||||
id: ","
|
id: ","
|
||||||
|
keyCode: Comma
|
||||||
title: Comma
|
title: Comma
|
||||||
45:
|
45:
|
||||||
id: "-"
|
id: "-"
|
||||||
|
keyCode: Minus
|
||||||
title: Minus
|
title: Minus
|
||||||
46:
|
46:
|
||||||
id: "."
|
id: "."
|
||||||
|
keyCode: Period
|
||||||
title: Period
|
title: Period
|
||||||
47:
|
47:
|
||||||
id: "/"
|
id: "/"
|
||||||
|
keyCode: Slash
|
||||||
title: Forward Slash
|
title: Forward Slash
|
||||||
48:
|
48:
|
||||||
id: "0"
|
id: "0"
|
||||||
|
keyCode: Digit0
|
||||||
title: Zero
|
title: Zero
|
||||||
49:
|
49:
|
||||||
id: "1"
|
id: "1"
|
||||||
|
keyCode: Digit1
|
||||||
title: One
|
title: One
|
||||||
50:
|
50:
|
||||||
id: "2"
|
id: "2"
|
||||||
|
keyCode: Digit2
|
||||||
title: Two
|
title: Two
|
||||||
51:
|
51:
|
||||||
id: "3"
|
id: "3"
|
||||||
|
keyCode: Digit3
|
||||||
title: Three
|
title: Three
|
||||||
52:
|
52:
|
||||||
id: "4"
|
id: "4"
|
||||||
|
keyCode: Digit4
|
||||||
title: Four
|
title: Four
|
||||||
53:
|
53:
|
||||||
id: "5"
|
id: "5"
|
||||||
|
keyCode: Digit5
|
||||||
title: Five
|
title: Five
|
||||||
54:
|
54:
|
||||||
id: "6"
|
id: "6"
|
||||||
|
keyCode: Digit6
|
||||||
title: Six
|
title: Six
|
||||||
55:
|
55:
|
||||||
id: "7"
|
id: "7"
|
||||||
|
keyCode: Digit7
|
||||||
title: Seven
|
title: Seven
|
||||||
56:
|
56:
|
||||||
id: "8"
|
id: "8"
|
||||||
|
keyCode: Digit8
|
||||||
title: Eight
|
title: Eight
|
||||||
57:
|
57:
|
||||||
id: "9"
|
id: "9"
|
||||||
|
keyCode: Digit9
|
||||||
title: Nine
|
title: Nine
|
||||||
58:
|
|
||||||
id: ":"
|
|
||||||
title: Colon
|
|
||||||
59:
|
59:
|
||||||
id: ";"
|
id: ";"
|
||||||
|
keyCode: Semicolon
|
||||||
title: Semicolon
|
title: Semicolon
|
||||||
60:
|
|
||||||
id: "<"
|
|
||||||
title: Less Than
|
|
||||||
61:
|
61:
|
||||||
id: "="
|
id: "="
|
||||||
|
keyCode: Equal
|
||||||
title: Equals
|
title: Equals
|
||||||
62:
|
|
||||||
id: ">"
|
|
||||||
title: Greater Than
|
|
||||||
63:
|
|
||||||
id: "?"
|
|
||||||
title: Question Mark
|
|
||||||
64:
|
|
||||||
id: "@"
|
|
||||||
title: At Symbol
|
|
||||||
65:
|
|
||||||
id: "A"
|
|
||||||
title: Uppercase A
|
|
||||||
66:
|
|
||||||
id: "B"
|
|
||||||
title: Uppercase B
|
|
||||||
67:
|
|
||||||
id: "C"
|
|
||||||
title: Uppercase C
|
|
||||||
68:
|
|
||||||
id: "D"
|
|
||||||
title: Uppercase D
|
|
||||||
69:
|
|
||||||
id: "E"
|
|
||||||
title: Uppercase E
|
|
||||||
70:
|
|
||||||
id: "F"
|
|
||||||
title: Uppercase F
|
|
||||||
71:
|
|
||||||
id: "G"
|
|
||||||
title: Uppercase G
|
|
||||||
72:
|
|
||||||
id: "H"
|
|
||||||
title: Uppercase H
|
|
||||||
73:
|
|
||||||
id: "I"
|
|
||||||
title: Uppercase I
|
|
||||||
74:
|
|
||||||
id: "J"
|
|
||||||
title: Uppercase J
|
|
||||||
75:
|
|
||||||
id: "K"
|
|
||||||
title: Uppercase K
|
|
||||||
76:
|
|
||||||
id: "L"
|
|
||||||
title: Uppercase L
|
|
||||||
77:
|
|
||||||
id: "M"
|
|
||||||
title: Uppercase M
|
|
||||||
78:
|
|
||||||
id: "N"
|
|
||||||
title: Uppercase N
|
|
||||||
79:
|
|
||||||
id: "O"
|
|
||||||
title: Uppercase O
|
|
||||||
80:
|
|
||||||
id: "P"
|
|
||||||
title: Uppercase P
|
|
||||||
81:
|
|
||||||
id: "Q"
|
|
||||||
title: Uppercase Q
|
|
||||||
82:
|
|
||||||
id: "R"
|
|
||||||
title: Uppercase R
|
|
||||||
83:
|
|
||||||
id: "S"
|
|
||||||
title: Uppercase S
|
|
||||||
84:
|
|
||||||
id: "T"
|
|
||||||
title: Uppercase T
|
|
||||||
85:
|
|
||||||
id: "U"
|
|
||||||
title: Uppercase U
|
|
||||||
86:
|
|
||||||
id: "V"
|
|
||||||
title: Uppercase V
|
|
||||||
87:
|
|
||||||
id: "W"
|
|
||||||
title: Uppercase W
|
|
||||||
88:
|
|
||||||
id: "X"
|
|
||||||
title: Uppercase X
|
|
||||||
89:
|
|
||||||
id: "Y"
|
|
||||||
title: Uppercase Y
|
|
||||||
90:
|
|
||||||
id: "Z"
|
|
||||||
title: Uppercase Z
|
|
||||||
91:
|
91:
|
||||||
id: "["
|
id: "["
|
||||||
|
keyCode: BracketLeft
|
||||||
title: Left Bracket
|
title: Left Bracket
|
||||||
92:
|
92:
|
||||||
id: "\\"
|
id: "\\"
|
||||||
|
keyCode: Backslash
|
||||||
title: Backslash
|
title: Backslash
|
||||||
93:
|
93:
|
||||||
id: "]"
|
id: "]"
|
||||||
|
keyCode: BracketRight
|
||||||
title: Right Bracket
|
title: Right Bracket
|
||||||
94:
|
|
||||||
id: "^"
|
|
||||||
title: Caret
|
|
||||||
95:
|
|
||||||
id: "_"
|
|
||||||
title: Underscore
|
|
||||||
96:
|
96:
|
||||||
id: "`"
|
id: "`"
|
||||||
|
keyCode: Backquote
|
||||||
title: Backtick
|
title: Backtick
|
||||||
97:
|
97:
|
||||||
id: "a"
|
id: "a"
|
||||||
|
keyCode: KeyA
|
||||||
title: Lowercase a
|
title: Lowercase a
|
||||||
98:
|
98:
|
||||||
id: "b"
|
id: "b"
|
||||||
|
keyCode: KeyB
|
||||||
title: Lowercase b
|
title: Lowercase b
|
||||||
99:
|
99:
|
||||||
id: "c"
|
id: "c"
|
||||||
|
keyCode: KeyC
|
||||||
title: Lowercase c
|
title: Lowercase c
|
||||||
100:
|
100:
|
||||||
id: "d"
|
id: "d"
|
||||||
|
keyCode: KeyD
|
||||||
title: Lowercase d
|
title: Lowercase d
|
||||||
101:
|
101:
|
||||||
id: "e"
|
id: "e"
|
||||||
|
keyCode: KeyE
|
||||||
title: Lowercase e
|
title: Lowercase e
|
||||||
102:
|
102:
|
||||||
id: "f"
|
id: "f"
|
||||||
|
keyCode: KeyF
|
||||||
title: Lowercase f
|
title: Lowercase f
|
||||||
103:
|
103:
|
||||||
id: "g"
|
id: "g"
|
||||||
|
keyCode: KeyG
|
||||||
title: Lowercase g
|
title: Lowercase g
|
||||||
104:
|
104:
|
||||||
id: "h"
|
id: "h"
|
||||||
|
keyCode: KeyH
|
||||||
title: Lowercase h
|
title: Lowercase h
|
||||||
105:
|
105:
|
||||||
id: "i"
|
id: "i"
|
||||||
|
keyCode: KeyI
|
||||||
title: Lowercase i
|
title: Lowercase i
|
||||||
106:
|
106:
|
||||||
id: "j"
|
id: "j"
|
||||||
|
keyCode: KeyJ
|
||||||
title: Lowercase j
|
title: Lowercase j
|
||||||
107:
|
107:
|
||||||
id: "k"
|
id: "k"
|
||||||
|
keyCode: KeyK
|
||||||
title: Lowercase k
|
title: Lowercase k
|
||||||
108:
|
108:
|
||||||
id: "l"
|
id: "l"
|
||||||
|
keyCode: KeyL
|
||||||
title: Lowercase l
|
title: Lowercase l
|
||||||
109:
|
109:
|
||||||
id: "m"
|
id: "m"
|
||||||
|
keyCode: KeyM
|
||||||
title: Lowercase m
|
title: Lowercase m
|
||||||
110:
|
110:
|
||||||
id: "n"
|
id: "n"
|
||||||
|
keyCode: KeyN
|
||||||
title: Lowercase n
|
title: Lowercase n
|
||||||
111:
|
111:
|
||||||
id: "o"
|
id: "o"
|
||||||
|
keyCode: KeyO
|
||||||
title: Lowercase o
|
title: Lowercase o
|
||||||
112:
|
112:
|
||||||
id: "p"
|
id: "p"
|
||||||
|
keyCode: KeyP
|
||||||
title: Lowercase p
|
title: Lowercase p
|
||||||
113:
|
113:
|
||||||
id: "q"
|
id: "q"
|
||||||
|
keyCode: KeyQ
|
||||||
title: Lowercase q
|
title: Lowercase q
|
||||||
114:
|
114:
|
||||||
id: "r"
|
id: "r"
|
||||||
|
keyCode: KeyR
|
||||||
title: Lowercase r
|
title: Lowercase r
|
||||||
115:
|
115:
|
||||||
id: "s"
|
id: "s"
|
||||||
|
keyCode: KeyS
|
||||||
title: Lowercase s
|
title: Lowercase s
|
||||||
116:
|
116:
|
||||||
id: "t"
|
id: "t"
|
||||||
|
keyCode: KeyT
|
||||||
title: Lowercase t
|
title: Lowercase t
|
||||||
117:
|
117:
|
||||||
id: "u"
|
id: "u"
|
||||||
|
keyCode: KeyU
|
||||||
title: Lowercase u
|
title: Lowercase u
|
||||||
118:
|
118:
|
||||||
id: "v"
|
id: "v"
|
||||||
|
keyCode: KeyV
|
||||||
title: Lowercase v
|
title: Lowercase v
|
||||||
119:
|
119:
|
||||||
id: "w"
|
id: "w"
|
||||||
|
KeyCode: KeyW
|
||||||
title: Lowercase w
|
title: Lowercase w
|
||||||
120:
|
120:
|
||||||
id: "x"
|
id: "x"
|
||||||
|
keyCode: KeyX
|
||||||
title: Lowercase x
|
title: Lowercase x
|
||||||
121:
|
121:
|
||||||
id: "y"
|
id: "y"
|
||||||
|
keyCode: KeyY
|
||||||
title: Lowercase y
|
title: Lowercase y
|
||||||
122:
|
122:
|
||||||
id: "z"
|
id: "z"
|
||||||
|
keyCode: KeyZ
|
||||||
title: Lowercase z
|
title: Lowercase z
|
||||||
123:
|
|
||||||
id: "{"
|
|
||||||
title: Left Curly Brace
|
|
||||||
124:
|
|
||||||
id: "|"
|
|
||||||
title: Pipe
|
|
||||||
125:
|
|
||||||
id: "}"
|
|
||||||
title: Right Curly Brace
|
|
||||||
126:
|
|
||||||
id: "~"
|
|
||||||
title: Tilde
|
|
||||||
127:
|
127:
|
||||||
id: "DEL"
|
id: "DEL"
|
||||||
|
keyCode: Delete
|
||||||
title: Delete
|
title: Delete
|
||||||
icon: delete_forever
|
|
||||||
@@ -6,34 +6,73 @@ type: unassigned
|
|||||||
actions:
|
actions:
|
||||||
600:
|
600:
|
||||||
id: "LH_THUMB_3_3D"
|
id: "LH_THUMB_3_3D"
|
||||||
title: Left Hand Thumb Top 3D Click
|
title: "Left Hand Thumb Bottom 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
601:
|
601:
|
||||||
id: "LH_THUMB_2_3D"
|
id: "LH_THUMB_2_3D"
|
||||||
title: Left Hand Thumb Middle 3D Click
|
title: "Left Hand Thumb Middle 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
602:
|
602:
|
||||||
id: "LH_THUMB_1_3D"
|
id: "LH_THUMB_1_3D"
|
||||||
title: Left Hand Thumb Bottom 3D Click
|
title: "Left Hand Thumb Top 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
603:
|
603:
|
||||||
id: "LH_INDEX_3D"
|
id: "LH_INDEX_3D"
|
||||||
title: Left Hand Index Finger 3D Click
|
title: "Left Hand Index Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
604:
|
604:
|
||||||
id: "LH_MID_1_3D"
|
id: "LH_MID_1_3D"
|
||||||
title: Left Hand Middle Finger 3D Click
|
title: "Left Hand Middle Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
605:
|
605:
|
||||||
id: "LH_RING_1_3D"
|
id: "LH_RING_1_3D"
|
||||||
title: Left Hand Ring Finger 3D Click
|
title: "Left Hand Ring Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
606:
|
606:
|
||||||
id: "LH_PINKY_3D"
|
id: "LH_PINKY_3D"
|
||||||
title: Left Hand Pinky 3D Click,
|
title: "Left Hand Pinky 3D Click"
|
||||||
# TODO...
|
icon: "adjust"
|
||||||
# ["607", "CharaChorder One", "LH_MID_2_3D", "", ""],
|
607:
|
||||||
# ["608", "CharaChorder One", "LH_RING_2_3D", "", ""],
|
id: "LH_MID_2_3D"
|
||||||
# ["609", "CharaChorder One", "RH_THUMB_3_3D", "", ""],
|
title: "Left Hand Middle Finger 2 3D Click"
|
||||||
# ["610", "CharaChorder One", "RH_THUMB_2_3D", "", ""],
|
icon: "adjust"
|
||||||
# ["611", "CharaChorder One", "RH_THUMB_1_3D", "", ""],
|
608:
|
||||||
# ["612", "CharaChorder One", "RH_INDEX_3D", "", ""],
|
id: "LH_RING_2_3D"
|
||||||
# ["613", "CharaChorder One", "RH_MID_1_3D", "", ""],
|
title: "Left Hand Ring Finger 2 3D Click"
|
||||||
# ["614", "CharaChorder One", "RH_RING_1_3D", "", ""],
|
icon: "adjust"
|
||||||
# ["615", "CharaChorder One", "RH_PINKY_3D", "", ""],
|
609:
|
||||||
# ["616", "CharaChorder One", "RH_MID_2_3D", "", ""],
|
id: "RH_THUMB_3_3D"
|
||||||
# ["617", "CharaChorder One", "RH_RING_2_3D", "", ""]
|
title: "Right Hand Thumb Bottom 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
610:
|
||||||
|
id: "RH_THUMB_2_3D"
|
||||||
|
title: "Right Hand Thumb Middle 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
611:
|
||||||
|
id: "RH_THUMB_1_3D"
|
||||||
|
title: "Right Hand Thumb Top 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
612:
|
||||||
|
id: "RH_INDEX_3D"
|
||||||
|
title: "Right Hand Index Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
613:
|
||||||
|
id: "RH_MID_1_3D"
|
||||||
|
title: "Right Hand Middle Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
614:
|
||||||
|
id: "RH_RING_1_3D"
|
||||||
|
title: "Right Hand Ring Finger 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
615:
|
||||||
|
id: "RH_PINKY_3D"
|
||||||
|
title: "Right Hand Pinky 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
616:
|
||||||
|
id: "RH_MID_2_3D"
|
||||||
|
title: "Right Hand Middle Finger 2 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
617:
|
||||||
|
id: "RH_RING_2_3D"
|
||||||
|
title: "Right Hand Ring Finger 2 3D Click"
|
||||||
|
icon: "adjust"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
name: CharaChorder
|
name: CharaChorder
|
||||||
description: CharaChorder specific actions
|
description: CharaChorder specific actions
|
||||||
actions:
|
actions:
|
||||||
|
0:
|
||||||
|
id: "NO_ACTION"
|
||||||
|
display: "No Action"
|
||||||
528:
|
528:
|
||||||
id: "RESTART"
|
id: "RESTART"
|
||||||
title: Restart Device
|
title: Restart Device
|
||||||
@@ -26,7 +29,7 @@ actions:
|
|||||||
536:
|
536:
|
||||||
id: "DUP"
|
id: "DUP"
|
||||||
title: Repeat Last Note
|
title: Repeat Last Note
|
||||||
icon: control_point_duplicate
|
icon: copy_all
|
||||||
description: |
|
description: |
|
||||||
In character entry, it repeats your last input.
|
In character entry, it repeats your last input.
|
||||||
In chorded entry, it is used for words with repeating letters.
|
In chorded entry, it is used for words with repeating letters.
|
||||||
@@ -58,6 +61,7 @@ actions:
|
|||||||
544:
|
544:
|
||||||
variantOf: 36
|
variantOf: 36
|
||||||
id: "SPACERIGHT"
|
id: "SPACERIGHT"
|
||||||
|
display: " "
|
||||||
title: Right Spacebar (eg CC Lite)
|
title: Right Spacebar (eg CC Lite)
|
||||||
icon: space_bar
|
icon: space_bar
|
||||||
variant: right
|
variant: right
|
||||||
@@ -66,6 +70,9 @@ actions:
|
|||||||
title: Primary Keymap
|
title: Primary Keymap
|
||||||
icon: counter_1
|
icon: counter_1
|
||||||
variant: left
|
variant: left
|
||||||
|
description: |
|
||||||
|
Acts as a toggle if the same action is not assigned
|
||||||
|
to the target layer
|
||||||
549:
|
549:
|
||||||
variantOf: 548
|
variantOf: 548
|
||||||
<<: *primary_keymap
|
<<: *primary_keymap
|
||||||
@@ -76,6 +83,9 @@ actions:
|
|||||||
title: Numeric Layer
|
title: Numeric Layer
|
||||||
icon: counter_2
|
icon: counter_2
|
||||||
variant: left
|
variant: left
|
||||||
|
description: |
|
||||||
|
Acts as a toggle if the same action is not assigned
|
||||||
|
to the target layer
|
||||||
551:
|
551:
|
||||||
variantOf: 550
|
variantOf: 550
|
||||||
<<: *secondary_keymap
|
<<: *secondary_keymap
|
||||||
@@ -86,8 +96,44 @@ actions:
|
|||||||
title: Function Layer
|
title: Function Layer
|
||||||
icon: counter_3
|
icon: counter_3
|
||||||
variant: left
|
variant: left
|
||||||
|
description: |
|
||||||
|
Acts as a toggle if the same action is not assigned
|
||||||
|
to the target layer
|
||||||
553:
|
553:
|
||||||
variationOf: 552
|
variationOf: 552
|
||||||
<<: *tertiary_keymap
|
<<: *tertiary_keymap
|
||||||
id: "KM_3_R"
|
id: "KM_3_R"
|
||||||
variant: right
|
variant: right
|
||||||
|
558:
|
||||||
|
id: HOLD_COMPOUND
|
||||||
|
title: Dynamic Library
|
||||||
|
icon: layers
|
||||||
|
description: |
|
||||||
|
Allows for the activation & creation of dynamic chord libraries.
|
||||||
|
When included as part of a chord output,
|
||||||
|
that chord's input becomes the seed for a dynamic chord library,
|
||||||
|
and that library is activated.
|
||||||
|
Any new chords created while a dynamic library is active are established one level above its seed.
|
||||||
|
559:
|
||||||
|
id: RELEASE_COMPOUND
|
||||||
|
title: Base Library
|
||||||
|
icon: layers_clear
|
||||||
|
description: |
|
||||||
|
Re-activates your base chord library,
|
||||||
|
and deactivates any currently active dynamic chord library.
|
||||||
|
576:
|
||||||
|
id: ACTION_DELAY_1000
|
||||||
|
icon: clock_loader_90
|
||||||
|
description: Wait for one second
|
||||||
|
577:
|
||||||
|
id: ACTION_DELAY_100
|
||||||
|
icon: clock_loader_60
|
||||||
|
description: Wait for 100 milliseconds
|
||||||
|
578:
|
||||||
|
id: ACTION_DELAY_10
|
||||||
|
icon: clock_loader_40
|
||||||
|
description: Wait for 10 milliseconds
|
||||||
|
579:
|
||||||
|
id: ACTION_DELAY_1
|
||||||
|
icon: clock_loader_10
|
||||||
|
description: Wait for one millisecond
|
||||||
|
|||||||
@@ -4,41 +4,52 @@ icon: keyboard
|
|||||||
actions:
|
actions:
|
||||||
512: &left_ctrl
|
512: &left_ctrl
|
||||||
id: "LEFT_CTRL"
|
id: "LEFT_CTRL"
|
||||||
|
display: CTRL
|
||||||
title: Control Keyboard Modifier
|
title: Control Keyboard Modifier
|
||||||
|
keyCode: ControlLeft
|
||||||
variant: left
|
variant: left
|
||||||
icon: keyboard_control_key
|
|
||||||
513: &left_shift
|
513: &left_shift
|
||||||
id: "LEFT_SHIFT"
|
id: "LEFT_SHIFT"
|
||||||
title: Shift Keyboard Modifier
|
title: Shift Keyboard Modifier
|
||||||
|
keyCode: ShiftLeft
|
||||||
variant: left
|
variant: left
|
||||||
icon: shift
|
icon: shift
|
||||||
514: &left_alt
|
514: &left_alt
|
||||||
id: "LEFT_ALT"
|
id: "LEFT_ALT"
|
||||||
|
display: ALT
|
||||||
title: Alt Keyboard Modifier
|
title: Alt Keyboard Modifier
|
||||||
|
keyCode: AltLeft
|
||||||
variant: left
|
variant: left
|
||||||
icon: keyboard_option_key
|
|
||||||
515: &left_gui
|
515: &left_gui
|
||||||
id: "LEFT_GUI"
|
id: "LEFT_GUI"
|
||||||
title: GUI Keyboard Modifier
|
title: GUI Keyboard Modifier
|
||||||
|
keyCode: MetaLeft
|
||||||
|
icon: apps
|
||||||
variant: left
|
variant: left
|
||||||
icon: keyboard_command_key
|
|
||||||
516:
|
516:
|
||||||
variationOf: 512
|
variationOf: 512
|
||||||
<<: *left_ctrl
|
<<: *left_ctrl
|
||||||
id: "RIGHT_CTRL"
|
id: "RIGHT_CTRL"
|
||||||
|
keyCode: ControlRight
|
||||||
variant: right
|
variant: right
|
||||||
517:
|
517:
|
||||||
variationOf: 513
|
variationOf: 513
|
||||||
<<: *left_shift
|
<<: *left_shift
|
||||||
id: "RIGHT_SHIFT"
|
id: "RIGHT_SHIFT"
|
||||||
|
keyCode: ShiftRight
|
||||||
|
variant: right
|
||||||
518:
|
518:
|
||||||
variationOf: 514
|
variationOf: 514
|
||||||
<<: *left_alt
|
<<: *left_alt
|
||||||
id: "RIGHT_ALT"
|
id: "RIGHT_ALT"
|
||||||
|
keyCode: AltRight
|
||||||
|
variant: right
|
||||||
519:
|
519:
|
||||||
variationOf: 515
|
variationOf: 515
|
||||||
<<: *left_gui
|
<<: *left_gui
|
||||||
id: "RIGHT_GUI"
|
id: "RIGHT_GUI"
|
||||||
|
keyCode: MetaRight
|
||||||
|
variant: right
|
||||||
520:
|
520:
|
||||||
id: "RELEASE_MOD"
|
id: "RELEASE_MOD"
|
||||||
title: Release all keyboard modifiers
|
title: Release all keyboard modifiers
|
||||||
@@ -51,3 +62,11 @@ actions:
|
|||||||
id: "RELEASE_KEYS"
|
id: "RELEASE_KEYS"
|
||||||
title: Release all keys, but not keyboard modifiers
|
title: Release all keys, but not keyboard modifiers
|
||||||
icon: text_rotate_up
|
icon: text_rotate_up
|
||||||
|
523:
|
||||||
|
id: "PRESS_NEXT"
|
||||||
|
title: "Press and do not release the next key/action"
|
||||||
|
icon: download
|
||||||
|
524:
|
||||||
|
id: "RELEASE_NEXT"
|
||||||
|
title: "Release the next key/action in the sequence"
|
||||||
|
icon: upload
|
||||||
|
|||||||
25
src/lib/assets/keymaps/keymap.d.ts
vendored
@@ -1,16 +1,19 @@
|
|||||||
export interface KeymapCategory {
|
export interface KeymapCategory {
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
icon?: string
|
icon?: string;
|
||||||
type?: "unassigned"
|
display?: string;
|
||||||
actions: Record<number, Partial<ActionInfo>>
|
type?: "unassigned";
|
||||||
|
actions: Record<number, Partial<ActionInfo>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionInfo {
|
export interface ActionInfo {
|
||||||
id: string
|
id: string;
|
||||||
title: string
|
title: string;
|
||||||
icon: string
|
icon: string;
|
||||||
description: string
|
display: string;
|
||||||
variant: "left" | "right"
|
description: string;
|
||||||
variantOf: number
|
variant: "left" | "right";
|
||||||
|
variantOf: number;
|
||||||
|
keyCode: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
name: Raw Scancodes
|
name: Key codes
|
||||||
description: Raw Keyboard Scancodes
|
description: OS-Layout sensitive keycodes
|
||||||
actions:
|
actions:
|
||||||
256:
|
256:
|
||||||
id: "KSC_00"
|
id: "KSC_00"
|
||||||
|
icon: block
|
||||||
title: No Key Pressed
|
title: No Key Pressed
|
||||||
|
description: Also commonly used at the end of a chord to remove auto-spaces
|
||||||
257:
|
257:
|
||||||
id: "KSC_01"
|
id: "KSC_01"
|
||||||
title: Keyboard Error Roll Over
|
title: Keyboard Error Roll Over
|
||||||
@@ -15,322 +17,421 @@ actions:
|
|||||||
title: Keyboard Error Undefined
|
title: Keyboard Error Undefined
|
||||||
260:
|
260:
|
||||||
id: "KEY_A"
|
id: "KEY_A"
|
||||||
|
keyCode: "KeyA"
|
||||||
title: Keyboard a and A (US English)
|
title: Keyboard a and A (US English)
|
||||||
description: Non US English keyboard users may prefer these Raw Scancodes
|
description: Non US English keyboard users may prefer these Raw Scancodes
|
||||||
261:
|
261:
|
||||||
id: "KEY_B"
|
id: "KEY_B"
|
||||||
|
keyCode: "KeyB"
|
||||||
title: Keyboard b and B (US English)
|
title: Keyboard b and B (US English)
|
||||||
262:
|
262:
|
||||||
id: "KEY_C"
|
id: "KEY_C"
|
||||||
|
keyCode: "KeyC"
|
||||||
title: Keyboard c and C (US English)
|
title: Keyboard c and C (US English)
|
||||||
263:
|
263:
|
||||||
id: "KEY_D"
|
id: "KEY_D"
|
||||||
|
keyCode: "KeyD"
|
||||||
title: Keyboard d and D (US English)
|
title: Keyboard d and D (US English)
|
||||||
264:
|
264:
|
||||||
id: "KEY_E"
|
id: "KEY_E"
|
||||||
|
keyCode: "KeyE"
|
||||||
title: Keyboard e and E (US English)
|
title: Keyboard e and E (US English)
|
||||||
265:
|
265:
|
||||||
id: "KEY_F"
|
id: "KEY_F"
|
||||||
|
keyCode: "KeyF"
|
||||||
title: Keyboard f and F (US English)
|
title: Keyboard f and F (US English)
|
||||||
266:
|
266:
|
||||||
id: "KEY_G"
|
id: "KEY_G"
|
||||||
|
keyCode: "KeyG"
|
||||||
title: Keyboard g and G (US English)
|
title: Keyboard g and G (US English)
|
||||||
267:
|
267:
|
||||||
id: "KEY_H"
|
id: "KEY_H"
|
||||||
|
keyCode: "KeyH"
|
||||||
title: Keyboard h and H (US English)
|
title: Keyboard h and H (US English)
|
||||||
268:
|
268:
|
||||||
id: "KEY_I"
|
id: "KEY_I"
|
||||||
|
keyCode: "KeyI"
|
||||||
title: Keyboard i and I (US English)
|
title: Keyboard i and I (US English)
|
||||||
269:
|
269:
|
||||||
id: "KEY_J"
|
id: "KEY_J"
|
||||||
|
keyCode: "KeyJ"
|
||||||
title: Keyboard j and J (US English)
|
title: Keyboard j and J (US English)
|
||||||
270:
|
270:
|
||||||
id: "KEY_K"
|
id: "KEY_K"
|
||||||
|
keyCode: "KeyK"
|
||||||
title: Keyboard k and K (US English)
|
title: Keyboard k and K (US English)
|
||||||
271:
|
271:
|
||||||
id: "KEY_L"
|
id: "KEY_L"
|
||||||
|
keyCode: "KeyL"
|
||||||
title: Keyboard l and L (US English)
|
title: Keyboard l and L (US English)
|
||||||
272:
|
272:
|
||||||
id: "KEY_M"
|
id: "KEY_M"
|
||||||
|
keyCode: "KeyM"
|
||||||
title: Keyboard m and M (US English)
|
title: Keyboard m and M (US English)
|
||||||
273:
|
273:
|
||||||
id: "KEY_N"
|
id: "KEY_N"
|
||||||
|
keyCode: "KeyN"
|
||||||
title: Keyboard n and N (US English)
|
title: Keyboard n and N (US English)
|
||||||
274:
|
274:
|
||||||
id: "KEY_O"
|
id: "KEY_O"
|
||||||
|
keyCode: "KeyO"
|
||||||
title: Keyboard o and O (US English)
|
title: Keyboard o and O (US English)
|
||||||
275:
|
275:
|
||||||
id: "KEY_P"
|
id: "KEY_P"
|
||||||
|
keyCode: "KeyP"
|
||||||
title: Keyboard p and P (US English)
|
title: Keyboard p and P (US English)
|
||||||
276:
|
276:
|
||||||
id: "KEY_Q"
|
id: "KEY_Q"
|
||||||
|
keyCode: "KeyQ"
|
||||||
title: Keyboard q and Q (US English)
|
title: Keyboard q and Q (US English)
|
||||||
277:
|
277:
|
||||||
id: "KEY_R"
|
id: "KEY_R"
|
||||||
|
keyCode: "KeyR"
|
||||||
title: Keyboard r and R (US English)
|
title: Keyboard r and R (US English)
|
||||||
278:
|
278:
|
||||||
id: "KEY_S"
|
id: "KEY_S"
|
||||||
|
keyCode: "KeyS"
|
||||||
title: Keyboard s and S (US English)
|
title: Keyboard s and S (US English)
|
||||||
279:
|
279:
|
||||||
id: "KEY_T"
|
id: "KEY_T"
|
||||||
|
keyCode: "KeyT"
|
||||||
title: Keyboard t and T (US English)
|
title: Keyboard t and T (US English)
|
||||||
280:
|
280:
|
||||||
id: "KEY_U"
|
id: "KEY_U"
|
||||||
|
keyCode: "KeyU"
|
||||||
title: Keyboard u and U (US English)
|
title: Keyboard u and U (US English)
|
||||||
281:
|
281:
|
||||||
id: "KEY_V"
|
id: "KEY_V"
|
||||||
|
keyCode: "KeyV"
|
||||||
title: Keyboard v and V (US English)
|
title: Keyboard v and V (US English)
|
||||||
282:
|
282:
|
||||||
id: "KEY_W"
|
id: "KEY_W"
|
||||||
|
keyCode: "KeyW"
|
||||||
title: Keyboard w and W (US English)
|
title: Keyboard w and W (US English)
|
||||||
283:
|
283:
|
||||||
id: "KEY_X"
|
id: "KEY_X"
|
||||||
|
keyCode: "KeyX"
|
||||||
title: Keyboard x and X (US English)
|
title: Keyboard x and X (US English)
|
||||||
284:
|
284:
|
||||||
id: "KEY_Y"
|
id: "KEY_Y"
|
||||||
|
keyCode: "KeyY"
|
||||||
title: Keyboard y and Y (US English)
|
title: Keyboard y and Y (US English)
|
||||||
285:
|
285:
|
||||||
id: "KEY_Z"
|
id: "KEY_Z"
|
||||||
|
keyCode: "KeyZ"
|
||||||
title: Keyboard z and Z (US English)
|
title: Keyboard z and Z (US English)
|
||||||
286:
|
286:
|
||||||
id: "KEY_1"
|
id: "KEY_1"
|
||||||
|
keyCode: "Digit1"
|
||||||
title: Keyboard 1 and ! (US English)
|
title: Keyboard 1 and ! (US English)
|
||||||
287:
|
287:
|
||||||
id: "KEY_2"
|
id: "KEY_2"
|
||||||
|
keyCode: "Digit2"
|
||||||
title: Keyboard 2 and @ (US English)
|
title: Keyboard 2 and @ (US English)
|
||||||
288:
|
288:
|
||||||
id: "KEY_3"
|
id: "KEY_3"
|
||||||
|
keyCode: "Digit3"
|
||||||
title: Keyboard 3 and # (US English)
|
title: Keyboard 3 and # (US English)
|
||||||
289:
|
289:
|
||||||
id: "KEY_4"
|
id: "KEY_4"
|
||||||
|
keyCode: "Digit4"
|
||||||
title: Keyboard 4 and $ (US English)
|
title: Keyboard 4 and $ (US English)
|
||||||
290:
|
290:
|
||||||
id: "KEY_5"
|
id: "KEY_5"
|
||||||
|
keyCode: "Digit5"
|
||||||
title: Keyboard 5 and % (US English)
|
title: Keyboard 5 and % (US English)
|
||||||
291:
|
291:
|
||||||
id: "KEY_6"
|
id: "KEY_6"
|
||||||
|
keyCode: "Digit6"
|
||||||
title: Keyboard 6 and ^ (US English)
|
title: Keyboard 6 and ^ (US English)
|
||||||
292:
|
292:
|
||||||
id: "KEY_7"
|
id: "KEY_7"
|
||||||
|
keyCode: "Digit7"
|
||||||
title: Keyboard 7 and & (US English)
|
title: Keyboard 7 and & (US English)
|
||||||
293:
|
293:
|
||||||
id: "KEY_8"
|
id: "KEY_8"
|
||||||
|
keyCode: "Digit8"
|
||||||
title: Keyboard 8 and * (US English)
|
title: Keyboard 8 and * (US English)
|
||||||
294:
|
294:
|
||||||
id: "KEY_9"
|
id: "KEY_9"
|
||||||
|
keyCode: "Digit9"
|
||||||
title: Keyboard 9 and ( (US English)
|
title: Keyboard 9 and ( (US English)
|
||||||
295:
|
295:
|
||||||
id: "KEY_0"
|
id: "KEY_0"
|
||||||
|
keyCode: "Digit0"
|
||||||
title: Keyboard 0 and ) (US English)
|
title: Keyboard 0 and ) (US English)
|
||||||
296:
|
296:
|
||||||
id: "ENTER"
|
id: "ENTER"
|
||||||
|
keyCode: "Enter"
|
||||||
title: Keyboard Return (US English)
|
title: Keyboard Return (US English)
|
||||||
icon: keyboard_return
|
icon: keyboard_return
|
||||||
297:
|
297:
|
||||||
id: "ESC"
|
id: "ESC"
|
||||||
|
keyCode: "Escape"
|
||||||
title: Keyboard Escape (US English)
|
title: Keyboard Escape (US English)
|
||||||
298:
|
298:
|
||||||
id: "BKSP"
|
id: "BKSP"
|
||||||
|
keyCode: "Backspace"
|
||||||
title: Keyboard Backspace (US English)
|
title: Keyboard Backspace (US English)
|
||||||
icon: backspace
|
icon: backspace
|
||||||
299:
|
299:
|
||||||
id: "TAB"
|
id: "TAB"
|
||||||
|
keyCode: "Tab"
|
||||||
title: Keyboard Tab (US English)
|
title: Keyboard Tab (US English)
|
||||||
icon: keyboard_tab
|
icon: keyboard_tab
|
||||||
300:
|
300:
|
||||||
id: "KSC_2C"
|
id: "KSC_2C"
|
||||||
|
keyCode: "Space"
|
||||||
title: Keyboard Space (US English)
|
title: Keyboard Space (US English)
|
||||||
description: |
|
description: |
|
||||||
The ASCII space is preferred over this raw scancode for the space bar.
|
The ASCII space is preferred over this raw scancode for the space bar.
|
||||||
icon: space_bar
|
icon: space_bar
|
||||||
301:
|
301:
|
||||||
id: "KSC_2D"
|
id: "KSC_2D"
|
||||||
|
keyCode: "Minus"
|
||||||
title: Keyboard - and _ (US English)
|
title: Keyboard - and _ (US English)
|
||||||
302:
|
302:
|
||||||
id: "KSC_2E"
|
id: "KSC_2E"
|
||||||
|
keyCode: "Equal"
|
||||||
title: Keyboard = and + (US English)
|
title: Keyboard = and + (US English)
|
||||||
303:
|
303:
|
||||||
id: "KSC_2F"
|
id: "KSC_2F"
|
||||||
|
keyCode: "BracketLeft"
|
||||||
title: Keyboard [ and { (US English)
|
title: Keyboard [ and { (US English)
|
||||||
304:
|
304:
|
||||||
id: "KSC_30"
|
id: "KSC_30"
|
||||||
|
keyCode: "BracketRight"
|
||||||
title: Keyboard ] and } (US English)
|
title: Keyboard ] and } (US English)
|
||||||
305:
|
305:
|
||||||
id: "KSC_31"
|
id: "KSC_31"
|
||||||
|
keyCode: "Backslash"
|
||||||
title: Keyboard \ and | (US English)
|
title: Keyboard \ and | (US English)
|
||||||
306:
|
306:
|
||||||
id: "KSC_32"
|
id: "KSC_32"
|
||||||
|
# TODO: also backslash?
|
||||||
title: Keyboard Non-US \# and ~ (US English)
|
title: Keyboard Non-US \# and ~ (US English)
|
||||||
307:
|
307:
|
||||||
id: "KSC_33"
|
id: "KSC_33"
|
||||||
|
keyCode: "Semicolon"
|
||||||
title: "Keyboard ; and : (US English)"
|
title: "Keyboard ; and : (US English)"
|
||||||
308:
|
308:
|
||||||
id: "KSC_34"
|
id: "KSC_34"
|
||||||
|
keyCode: "Quote"
|
||||||
title: Keyboard ' and " (US English)
|
title: Keyboard ' and " (US English)
|
||||||
309:
|
309:
|
||||||
id: "KSC_35"
|
id: "KSC_35"
|
||||||
|
keyCode: "Backquote"
|
||||||
title: Keyboard ` and ~ (US English)
|
title: Keyboard ` and ~ (US English)
|
||||||
310:
|
310:
|
||||||
id: "KSC_36"
|
id: "KSC_36"
|
||||||
|
keyCode: "Comma"
|
||||||
title: Keyboard , and < (US English)
|
title: Keyboard , and < (US English)
|
||||||
311:
|
311:
|
||||||
id: "KSC_37"
|
id: "KSC_37"
|
||||||
|
keyCode: "Period"
|
||||||
title: Keyboard . and > (US English)
|
title: Keyboard . and > (US English)
|
||||||
312:
|
312:
|
||||||
id: "KSC_38"
|
id: "KSC_38"
|
||||||
|
keyCode: "Slash"
|
||||||
title: Keyboard / and ? (US English)
|
title: Keyboard / and ? (US English)
|
||||||
313:
|
313:
|
||||||
id: "CAPSLOCK"
|
id: "CAPSLOCK"
|
||||||
|
keyCode: "CapsLock"
|
||||||
title: Keyboard Caps Lock
|
title: Keyboard Caps Lock
|
||||||
icon: shift_lock
|
icon: shift_lock
|
||||||
314:
|
314:
|
||||||
id: "F1"
|
id: "F1"
|
||||||
|
keyCode: "F1"
|
||||||
title: Keyboard F1
|
title: Keyboard F1
|
||||||
315:
|
315:
|
||||||
id: "F2"
|
id: "F2"
|
||||||
|
keyCode: "F2"
|
||||||
title: Keyboard F2
|
title: Keyboard F2
|
||||||
316:
|
316:
|
||||||
id: "F3"
|
id: "F3"
|
||||||
|
keyCode: "F3"
|
||||||
title: Keyboard F3
|
title: Keyboard F3
|
||||||
317:
|
317:
|
||||||
id: "F4"
|
id: "F4"
|
||||||
|
keyCode: "F4"
|
||||||
title: Keyboard F4
|
title: Keyboard F4
|
||||||
318:
|
318:
|
||||||
id: "F5"
|
id: "F5"
|
||||||
|
keyCode: "F5"
|
||||||
title: Keyboard F5
|
title: Keyboard F5
|
||||||
319:
|
319:
|
||||||
id: "F6"
|
id: "F6"
|
||||||
|
keyCode: "F6"
|
||||||
title: Keyboard F6
|
title: Keyboard F6
|
||||||
320:
|
320:
|
||||||
id: "F7"
|
id: "F7"
|
||||||
|
keyCode: "F7"
|
||||||
title: Keyboard F7
|
title: Keyboard F7
|
||||||
321:
|
321:
|
||||||
id: "F8"
|
id: "F8"
|
||||||
|
keyCode: "F8"
|
||||||
title: Keyboard F8
|
title: Keyboard F8
|
||||||
322:
|
322:
|
||||||
id: "F9"
|
id: "F9"
|
||||||
|
keyCode: "F9"
|
||||||
title: Keyboard F9
|
title: Keyboard F9
|
||||||
323:
|
323:
|
||||||
id: "F10"
|
id: "F10"
|
||||||
|
keyCode: "F10"
|
||||||
title: Keyboard F10
|
title: Keyboard F10
|
||||||
324:
|
324:
|
||||||
id: "F11"
|
id: "F11"
|
||||||
|
keyCode: "F11"
|
||||||
title: Keyboard F11
|
title: Keyboard F11
|
||||||
325:
|
325:
|
||||||
id: "F12"
|
id: "F12"
|
||||||
|
keyCode: "F12"
|
||||||
title: Keyboard F12
|
title: Keyboard F12
|
||||||
326:
|
326:
|
||||||
id: "PRTSCN"
|
id: "PRTSCN"
|
||||||
|
keyCode: "PrintScreen"
|
||||||
title: Keyboard Print Screen
|
title: Keyboard Print Screen
|
||||||
icon: screenshot_monitor
|
icon: screenshot_monitor
|
||||||
327:
|
327:
|
||||||
id: "SCRLK"
|
id: "SCRLK"
|
||||||
|
keyCode: "ScrollLock"
|
||||||
title: Keyboard Scroll Lock
|
title: Keyboard Scroll Lock
|
||||||
328:
|
328:
|
||||||
id: "PAUSE"
|
id: "PAUSE"
|
||||||
|
keyCode: "Pause"
|
||||||
title: Keyboard Pause
|
title: Keyboard Pause
|
||||||
329:
|
329:
|
||||||
id: "INSERT"
|
id: "INSERT"
|
||||||
|
keyCode: "Insert"
|
||||||
title: Keyboard Insert
|
title: Keyboard Insert
|
||||||
icon: insert_text
|
icon: insert_text
|
||||||
330:
|
330:
|
||||||
id: "HOME"
|
id: "HOME"
|
||||||
|
keyCode: "Home"
|
||||||
title: Keyboard Home
|
title: Keyboard Home
|
||||||
icon: home
|
icon: home
|
||||||
331:
|
331:
|
||||||
id: "PGUP"
|
id: "PGUP"
|
||||||
|
keyCode: "PageUp"
|
||||||
title: Keyboard Page Up
|
title: Keyboard Page Up
|
||||||
icon: move_up
|
icon: move_up
|
||||||
332:
|
332:
|
||||||
id: "DELETE"
|
id: "DELETE"
|
||||||
|
keyCode: "Delete"
|
||||||
title: Keyboard Delete Forward
|
title: Keyboard Delete Forward
|
||||||
333:
|
333:
|
||||||
id: "END"
|
id: "END"
|
||||||
|
keyCode: "End"
|
||||||
title: Keyboard End
|
title: Keyboard End
|
||||||
334:
|
334:
|
||||||
id: "PGDN"
|
id: "PGDN"
|
||||||
|
keyCode: "PageDown"
|
||||||
title: Keyboard Page Down
|
title: Keyboard Page Down
|
||||||
icon: move_down
|
icon: move_down
|
||||||
335:
|
335:
|
||||||
id: "ARROW_RT"
|
id: "ARROW_RT"
|
||||||
|
keyCode: "ArrowRight"
|
||||||
title: Keyboard Right Arrow
|
title: Keyboard Right Arrow
|
||||||
icon: keyboard_arrow_right
|
icon: keyboard_arrow_right
|
||||||
336:
|
336:
|
||||||
id: "ARROW_LF"
|
id: "ARROW_LF"
|
||||||
|
keyCode: "ArrowLeft"
|
||||||
title: Keyboard Left Arrow
|
title: Keyboard Left Arrow
|
||||||
icon: keyboard_arrow_left
|
icon: keyboard_arrow_left
|
||||||
337:
|
337:
|
||||||
id: "ARROW_DN"
|
id: "ARROW_DN"
|
||||||
|
keyCode: "ArrowDown"
|
||||||
title: Keyboard Down Arrow
|
title: Keyboard Down Arrow
|
||||||
icon: keyboard_arrow_down
|
icon: keyboard_arrow_down
|
||||||
338:
|
338:
|
||||||
id: "ARROW_UP"
|
id: "ARROW_UP"
|
||||||
|
keyCode: "ArrowUp"
|
||||||
title: Keyboard Up Arrow
|
title: Keyboard Up Arrow
|
||||||
icon: keyboard_arrow_up
|
icon: keyboard_arrow_up
|
||||||
339:
|
339:
|
||||||
id: "NUMLOCK"
|
id: "NUMLOCK"
|
||||||
|
keyCode: "NumLock"
|
||||||
title: Keyboard Num Lock and Clear
|
title: Keyboard Num Lock and Clear
|
||||||
340:
|
340:
|
||||||
id: "KP_SLASH"
|
id: "KP_SLASH"
|
||||||
|
keyCode: "NumpadDivide"
|
||||||
title: Keypad /
|
title: Keypad /
|
||||||
341:
|
341:
|
||||||
id: "KP_ASTER"
|
id: "KP_ASTER"
|
||||||
|
keyCode: "NumpadStar"
|
||||||
title: Keypad *
|
title: Keypad *
|
||||||
342:
|
342:
|
||||||
id: "KP_MINUS"
|
id: "KP_MINUS"
|
||||||
|
keyCode: "NumpadSubtract"
|
||||||
title: Keypad -
|
title: Keypad -
|
||||||
343:
|
343:
|
||||||
id: "KP_PLUS"
|
id: "KP_PLUS"
|
||||||
|
keyCode: "NumpadAdd"
|
||||||
title: Keypad +
|
title: Keypad +
|
||||||
344:
|
344:
|
||||||
id: "KP_ENTER"
|
id: "KP_ENTER"
|
||||||
|
keyCode: "NumpadEnter"
|
||||||
title: Keypad Enter
|
title: Keypad Enter
|
||||||
345:
|
345:
|
||||||
id: "KP_1"
|
id: "KP_1"
|
||||||
|
keyCode: "Numpad1"
|
||||||
title: Keypad 1 and End
|
title: Keypad 1 and End
|
||||||
346:
|
346:
|
||||||
id: "KP_2"
|
id: "KP_2"
|
||||||
|
keyCode: "Numpad2"
|
||||||
title: Keypad 2 and Down Arrow
|
title: Keypad 2 and Down Arrow
|
||||||
347:
|
347:
|
||||||
id: "KP_3"
|
id: "KP_3"
|
||||||
|
keyCode: "Numpad3"
|
||||||
title: Keypad 3 and Page Down
|
title: Keypad 3 and Page Down
|
||||||
348:
|
348:
|
||||||
id: "KP_4"
|
id: "KP_4"
|
||||||
|
keyCode: "Numpad4"
|
||||||
title: Keypad 4 and Left Arrow
|
title: Keypad 4 and Left Arrow
|
||||||
349:
|
349:
|
||||||
id: "KP_5"
|
id: "KP_5"
|
||||||
|
keyCode: "Numpad5"
|
||||||
title: Keypad 5
|
title: Keypad 5
|
||||||
350:
|
350:
|
||||||
id: "KP_6"
|
id: "KP_6"
|
||||||
|
keyCode: "Numpad6"
|
||||||
title: Keypad 6 and Rigth Arrow
|
title: Keypad 6 and Rigth Arrow
|
||||||
351:
|
351:
|
||||||
id: "KP_7"
|
id: "KP_7"
|
||||||
|
keyCode: "Numpad7"
|
||||||
title: Keypad 7 and Home
|
title: Keypad 7 and Home
|
||||||
352:
|
352:
|
||||||
id: "KP_8"
|
id: "KP_8"
|
||||||
|
keyCode: "Numpad8"
|
||||||
title: Keypad 8 and Up Arrow
|
title: Keypad 8 and Up Arrow
|
||||||
353:
|
353:
|
||||||
id: "KP_9"
|
id: "KP_9"
|
||||||
|
keyCode: "Numpad9"
|
||||||
title: Keypad 9 and Page Up
|
title: Keypad 9 and Page Up
|
||||||
354:
|
354:
|
||||||
id: "KP_0"
|
id: "KP_0"
|
||||||
|
keyCode: "Numpad0"
|
||||||
title: Keypad 0 and Insert
|
title: Keypad 0 and Insert
|
||||||
355:
|
355:
|
||||||
id: "KP_DOT"
|
id: "KP_DOT"
|
||||||
|
keyCode: "NumpadDecimal"
|
||||||
title: Keypad . and Delete
|
title: Keypad . and Delete
|
||||||
356:
|
356:
|
||||||
id: "KSC_64"
|
id: "KSC_64"
|
||||||
|
keyCode: "IntlBackslash"
|
||||||
title: Keyboard Non-US \ and | (US English)
|
title: Keyboard Non-US \ and | (US English)
|
||||||
357:
|
357:
|
||||||
id: "COMPOSE"
|
id: "COMPOSE"
|
||||||
|
icon: menu
|
||||||
title: Keyboard Application
|
title: Keyboard Application
|
||||||
description: Officially supported by Win, Unix, and Boot
|
|
||||||
358:
|
358:
|
||||||
id: "POWER"
|
id: "POWER"
|
||||||
|
keyCode: "Power"
|
||||||
title: Keyboard Power
|
title: Keyboard Power
|
||||||
description: Only officially supported by Mac and Unix
|
description: Only officially supported by Mac and Unix
|
||||||
359:
|
359:
|
||||||
id: "KP_EQUAL"
|
id: "KP_EQUAL"
|
||||||
|
keyCode: "NumpadEqual"
|
||||||
title: Keypad =
|
title: Keypad =
|
||||||
description: Only officially supported by Mac
|
description: Only officially supported by Mac
|
||||||
360:
|
360:
|
||||||
@@ -787,10 +888,12 @@ actions:
|
|||||||
description: Not required to be supported by any OS
|
description: Not required to be supported by any OS
|
||||||
472:
|
472:
|
||||||
id: "KSC_D8"
|
id: "KSC_D8"
|
||||||
|
keyCode: "NumpadClear"
|
||||||
title: Keypad Clear
|
title: Keypad Clear
|
||||||
description: Not required to be supported by any OS
|
description: Not required to be supported by any OS
|
||||||
473:
|
473:
|
||||||
id: "KSC_D9"
|
id: "KSC_D9"
|
||||||
|
keyCode: "NumpadClearEntry"
|
||||||
title: Keypad Clear Entry
|
title: Keypad Clear Entry
|
||||||
description: Not required to be supported by any OS
|
description: Not required to be supported by any OS
|
||||||
474:
|
474:
|
||||||
@@ -841,84 +944,99 @@ actions:
|
|||||||
title: Keyboard Right GUI
|
title: Keyboard Right GUI
|
||||||
488:
|
488:
|
||||||
id: "KSC_E8"
|
id: "KSC_E8"
|
||||||
|
icon: play_pause
|
||||||
|
keyCode: "MediaPlayPause"
|
||||||
title: Media Play Pause
|
title: Media Play Pause
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
|
||||||
489:
|
489:
|
||||||
id: "KSC_E9"
|
id: "KSC_E9"
|
||||||
|
icon: stop
|
||||||
|
keyCode: "MediaStop"
|
||||||
title: Media Stop CD
|
title: Media Stop CD
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
|
||||||
490:
|
490:
|
||||||
id: "KSC_EA"
|
id: "KSC_EA"
|
||||||
|
icon: skip_previous
|
||||||
|
keyCode: "MediaTrackPrevious"
|
||||||
title: Media Previous Song
|
title: Media Previous Song
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
|
||||||
491:
|
491:
|
||||||
id: "KSC_EB"
|
id: "KSC_EB"
|
||||||
|
icon: skip_next
|
||||||
|
keyCode: "MediaTrackNext"
|
||||||
title: Media Next Song
|
title: Media Next Song
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
|
||||||
492:
|
492:
|
||||||
id: "KSC_EC"
|
id: "KSC_EC"
|
||||||
|
icon: eject
|
||||||
|
keyCode: "Eject"
|
||||||
title: Media Eject CD
|
title: Media Eject CD
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: MacOS only
|
||||||
493:
|
493:
|
||||||
id: "KSC_ED"
|
id: "KSC_ED"
|
||||||
|
icon: volume_up
|
||||||
|
keyCode: "AudioVolumeUp"
|
||||||
title: Media Volume Up
|
title: Media Volume Up
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
|
||||||
494:
|
494:
|
||||||
id: "KSC_EE"
|
id: "KSC_EE"
|
||||||
|
icon: volume_down
|
||||||
|
keyCode: "AudioVolumeDown"
|
||||||
title: Media Volume Down
|
title: Media Volume Down
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
|
||||||
495:
|
495:
|
||||||
id: "KSC_EF"
|
id: "KSC_EF"
|
||||||
|
icon: volume_off
|
||||||
|
keyCode: "AudioVolumeMute"
|
||||||
title: Media Mute
|
title: Media Mute
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
|
||||||
496:
|
496:
|
||||||
id: "KSC_F0"
|
id: "KSC_F0"
|
||||||
title: Media www
|
icon: language
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
title: Media Browser
|
||||||
497:
|
497:
|
||||||
id: "KSC_F1"
|
id: "KSC_F1"
|
||||||
title: Media Back
|
keyCode: "BrowserBack"
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
title: Media Browser Back
|
||||||
498:
|
498:
|
||||||
id: "KSC_F2"
|
id: "KSC_F2"
|
||||||
title: Media Forward
|
keyCode: "BrowserForward"
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
title: Media Browser Forward
|
||||||
499:
|
499:
|
||||||
id: "KSC_F3"
|
id: "KSC_F3"
|
||||||
title: Media Stop
|
keyCode: "BrowserStop"
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
title: Media Browser Stop
|
||||||
|
description: Not supported on MacOS
|
||||||
500:
|
500:
|
||||||
id: "KSC_F4"
|
id: "KSC_F4"
|
||||||
title: Media Find
|
icon: search
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
keyCode: "BrowserSearch"
|
||||||
|
title: Media Browser Search
|
||||||
501:
|
501:
|
||||||
id: "KSC_F5"
|
id: "KSC_F5"
|
||||||
title: Media Scroll Up
|
icon: brightness_high
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
title: Media Brightness Up
|
||||||
502:
|
502:
|
||||||
id: "KSC_F6"
|
id: "KSC_F6"
|
||||||
title: Media Scroll Down
|
icon: brightness_low
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
title: Media Brightness Down
|
||||||
503:
|
503:
|
||||||
id: "KSC_F7"
|
id: "KSC_F7"
|
||||||
title: Media Edit
|
title: Media Edit
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
504:
|
504:
|
||||||
id: "KSC_F8"
|
id: "KSC_F8"
|
||||||
title: Media Sleep
|
icon: bedtime
|
||||||
|
keyCode: "Sleep"
|
||||||
|
title: Media System Sleep
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not required to be supported by any OS. Possibly deprecated.
|
||||||
505:
|
505:
|
||||||
id: "KSC_F9"
|
id: "KSC_F9"
|
||||||
title: Media Coffee
|
icon: routine
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
keyCode: "WakeUp"
|
||||||
|
title: Media System Wake
|
||||||
|
description: Not supported on Windows
|
||||||
506:
|
506:
|
||||||
id: "KSC_FA"
|
id: "KSC_FA"
|
||||||
title: Media Refresh
|
keyCode: "BrowserRefresh"
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
title: Media Browser Refresh
|
||||||
507:
|
507:
|
||||||
id: "KSC_FB"
|
id: "KSC_FB"
|
||||||
title: Media Calc
|
title: Media Calculator
|
||||||
description: Not required to be supported by any OS. Possibly deprecated.
|
description: Not supported on MacOS
|
||||||
508:
|
508:
|
||||||
id: "KSC_FC"
|
id: "KSC_FC"
|
||||||
description: Not required to be supported by any OS.
|
description: Not required to be supported by any OS.
|
||||||
@@ -930,4 +1048,4 @@ actions:
|
|||||||
description: Not required to be supported by any OS.
|
description: Not required to be supported by any OS.
|
||||||
511:
|
511:
|
||||||
id: "KSC_FF"
|
id: "KSC_FF"
|
||||||
description: Not required to be supported by any OS.
|
description: Not required to be supported by any OS.
|
||||||
|
|||||||
142
src/lib/assets/layouts/generic/103-key.yml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
name: 103-key
|
||||||
|
col:
|
||||||
|
- row:
|
||||||
|
- key: 41
|
||||||
|
- key: 58
|
||||||
|
offset: [1, 0]
|
||||||
|
- key: 59
|
||||||
|
- key: 60
|
||||||
|
- key: 61
|
||||||
|
- key: 62
|
||||||
|
offset: [0.5, 0]
|
||||||
|
- key: 63
|
||||||
|
- key: 64
|
||||||
|
- key: 65
|
||||||
|
- key: 66
|
||||||
|
offset: [0.5, 0]
|
||||||
|
- key: 67
|
||||||
|
- key: 68
|
||||||
|
- key: 69
|
||||||
|
- key: 70
|
||||||
|
offset: [0.25, 0]
|
||||||
|
- key: 71
|
||||||
|
- key: 72
|
||||||
|
- offset: [0, 0.25]
|
||||||
|
row:
|
||||||
|
- key: 53
|
||||||
|
- key: 30
|
||||||
|
- key: 31
|
||||||
|
- key: 32
|
||||||
|
- key: 33
|
||||||
|
- key: 34
|
||||||
|
- key: 35
|
||||||
|
- key: 36
|
||||||
|
- key: 37
|
||||||
|
- key: 38
|
||||||
|
- key: 39
|
||||||
|
- key: 45
|
||||||
|
- key: 46
|
||||||
|
- key: 42
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 73
|
||||||
|
offset: [0.25, 0]
|
||||||
|
- key: 74
|
||||||
|
- key: 75
|
||||||
|
- key: 83
|
||||||
|
offset: [0.25, 0]
|
||||||
|
- key: 84
|
||||||
|
- key: 85
|
||||||
|
- key: 86
|
||||||
|
- row:
|
||||||
|
- key: 43
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 20
|
||||||
|
- key: 26
|
||||||
|
- key: 8
|
||||||
|
- key: 21
|
||||||
|
- key: 23
|
||||||
|
- key: 28
|
||||||
|
- key: 24
|
||||||
|
- key: 12
|
||||||
|
- key: 18
|
||||||
|
- key: 19
|
||||||
|
- key: 47
|
||||||
|
- key: 48
|
||||||
|
- key: 40
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 76
|
||||||
|
offset: [0.25, 0]
|
||||||
|
- key: 77
|
||||||
|
- key: 78
|
||||||
|
- key: 95
|
||||||
|
offset: [0.25, 0]
|
||||||
|
- key: 96
|
||||||
|
- key: 97
|
||||||
|
- key: 87
|
||||||
|
size: [1, 2]
|
||||||
|
- offset: [0, -1]
|
||||||
|
row:
|
||||||
|
- key: 57
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 4
|
||||||
|
- key: 22
|
||||||
|
- key: 7
|
||||||
|
- key: 9
|
||||||
|
- key: 10
|
||||||
|
- key: 11
|
||||||
|
- key: 13
|
||||||
|
- key: 14
|
||||||
|
- key: 15
|
||||||
|
- key: 51
|
||||||
|
- key: 52
|
||||||
|
- key: 49
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 92
|
||||||
|
offset: [3.5, 0]
|
||||||
|
- key: 93
|
||||||
|
- key: 94
|
||||||
|
- row:
|
||||||
|
- key: 225
|
||||||
|
size: [2.5, 1]
|
||||||
|
- key: 29
|
||||||
|
- key: 27
|
||||||
|
- key: 6
|
||||||
|
- key: 25
|
||||||
|
- key: 5
|
||||||
|
- key: 17
|
||||||
|
- key: 16
|
||||||
|
- key: 54
|
||||||
|
- key: 55
|
||||||
|
- key: 56
|
||||||
|
- key: 229
|
||||||
|
size: [2.5, 1]
|
||||||
|
- key: 82
|
||||||
|
offset: [1.25, 0]
|
||||||
|
- key: 89
|
||||||
|
offset: [1.25, 0]
|
||||||
|
- key: 90
|
||||||
|
- key: 91
|
||||||
|
- key: 88
|
||||||
|
size: [1, 2]
|
||||||
|
- offset: [0, -1]
|
||||||
|
row:
|
||||||
|
- key: 224
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 227
|
||||||
|
- key: 226
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 44
|
||||||
|
size: [7, 1]
|
||||||
|
- key: 230
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 231
|
||||||
|
- key: 228
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 80
|
||||||
|
offset: [0.25, 0]
|
||||||
|
- key: 81
|
||||||
|
- key: 79
|
||||||
|
- key: 98
|
||||||
|
offset: [0.25, 0]
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 99
|
||||||
86
src/lib/assets/layouts/lite.yml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: Lite
|
||||||
|
col:
|
||||||
|
- row:
|
||||||
|
- key: 53
|
||||||
|
- key: 54
|
||||||
|
- key: 55
|
||||||
|
- key: 56
|
||||||
|
- key: 57
|
||||||
|
- key: 58
|
||||||
|
- key: 59
|
||||||
|
- key: 60
|
||||||
|
- key: 61
|
||||||
|
- key: 62
|
||||||
|
- key: 63
|
||||||
|
- key: 64
|
||||||
|
- key: 65
|
||||||
|
- key: 66
|
||||||
|
size: [2, 1]
|
||||||
|
- row:
|
||||||
|
- key: 39
|
||||||
|
size: [1.5, 1]
|
||||||
|
- key: 40
|
||||||
|
- key: 41
|
||||||
|
- key: 42
|
||||||
|
- key: 43
|
||||||
|
- key: 44
|
||||||
|
- key: 45
|
||||||
|
- key: 46
|
||||||
|
- key: 47
|
||||||
|
- key: 48
|
||||||
|
- key: 49
|
||||||
|
- key: 50
|
||||||
|
- key: 51
|
||||||
|
- key: 52
|
||||||
|
size: [1.5, 1]
|
||||||
|
- row:
|
||||||
|
- key: 26
|
||||||
|
size: [1.75, 1]
|
||||||
|
- key: 27
|
||||||
|
- key: 28
|
||||||
|
- key: 29
|
||||||
|
- key: 30
|
||||||
|
- key: 31
|
||||||
|
- key: 32
|
||||||
|
- key: 33
|
||||||
|
- key: 34
|
||||||
|
- key: 35
|
||||||
|
- key: 36
|
||||||
|
- key: 37
|
||||||
|
- key: 38
|
||||||
|
size: [2.25, 1]
|
||||||
|
- row:
|
||||||
|
- key: 12
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 13
|
||||||
|
- key: 14
|
||||||
|
- key: 15
|
||||||
|
- key: 16
|
||||||
|
- key: 17
|
||||||
|
- key: 18
|
||||||
|
- key: 19
|
||||||
|
- key: 20
|
||||||
|
- key: 21
|
||||||
|
- key: 22
|
||||||
|
- key: 23
|
||||||
|
- key: 24
|
||||||
|
- key: 25
|
||||||
|
- row:
|
||||||
|
- key: 0
|
||||||
|
- key: 1
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 2
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 3
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 4
|
||||||
|
- key: 5
|
||||||
|
- key: 6
|
||||||
|
size: [2, 1]
|
||||||
|
- key: 7
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 8
|
||||||
|
size: [1.25, 1]
|
||||||
|
- key: 9
|
||||||
|
- key: 10
|
||||||
|
- key: 11
|
||||||
37
src/lib/assets/layouts/m4g.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: M4G
|
||||||
|
col:
|
||||||
|
# Ring / Middle
|
||||||
|
- offset: [2, 0]
|
||||||
|
row:
|
||||||
|
- switch: { e: 26, n: 27, w: 28, s: 29 }
|
||||||
|
- switch: { e: 21, n: 22, w: 23, s: 24 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { w: 66, n: 67, e: 68, s: 69 }
|
||||||
|
- switch: { w: 71, n: 72, e: 73, s: 74 }
|
||||||
|
- offset: [2, 0]
|
||||||
|
row:
|
||||||
|
- switch: { e: 41, n: 42, w: 43, s: 44 }
|
||||||
|
- switch: { e: 36, n: 37, w: 38, s: 39 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { w: 81, n: 82, e: 83, s: 84 }
|
||||||
|
- switch: { w: 86, n: 87, e: 88, s: 89 }
|
||||||
|
# Pinkie / Index
|
||||||
|
- offset: [0, -3]
|
||||||
|
row:
|
||||||
|
- switch: { e: 31, n: 32, w: 33, s: 34 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { e: 16, n: 17, w: 18, s: 19 }
|
||||||
|
- switch: { w: 61, n: 62, e: 63, s: 64 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { w: 76, n: 77, e: 78, s: 79 }
|
||||||
|
# Thumbs
|
||||||
|
- row:
|
||||||
|
- offset: [5.5, 0.5]
|
||||||
|
switch: { e: 11, n: 12, w: 13, s: 14 }
|
||||||
|
- offset: [1, 0.5]
|
||||||
|
switch: { w: 56, n: 57, e: 58, s: 59 }
|
||||||
|
- row:
|
||||||
|
- offset: [4.5, -0.25]
|
||||||
|
switch: { e: 6, n: 7, w: 8, s: 9 }
|
||||||
|
- offset: [3, -0.25]
|
||||||
|
switch: { w: 51, n: 52, e: 53, s: 54 }
|
||||||
37
src/lib/assets/layouts/m4gr.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: M4G
|
||||||
|
col:
|
||||||
|
# Ring / Middle
|
||||||
|
- offset: [2, 0]
|
||||||
|
row:
|
||||||
|
- switch: { e: 26, n: 27, w: 28, s: 29 }
|
||||||
|
- switch: { e: 21, n: 22, w: 23, s: 24 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { w: 66, n: 67, e: 68, s: 69 }
|
||||||
|
- switch: { w: 71, n: 72, e: 73, s: 74 }
|
||||||
|
- offset: [2, 0]
|
||||||
|
row:
|
||||||
|
- switch: { e: 41, n: 42, w: 43, s: 44 }
|
||||||
|
- switch: { e: 36, n: 37, w: 38, s: 39 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { w: 81, n: 82, e: 83, s: 84 }
|
||||||
|
- switch: { w: 86, n: 87, e: 88, s: 89 }
|
||||||
|
# Pinkie / Index
|
||||||
|
- offset: [0, -3]
|
||||||
|
row:
|
||||||
|
- switch: { e: 31, n: 32, w: 33, s: 34 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { e: 16, n: 17, w: 18, s: 19 }
|
||||||
|
- switch: { w: 61, n: 62, e: 63, s: 64 }
|
||||||
|
- offset: [4, 0]
|
||||||
|
switch: { w: 76, n: 77, e: 78, s: 79 }
|
||||||
|
# Thumbs
|
||||||
|
- row:
|
||||||
|
- offset: [5.5, 0.5]
|
||||||
|
switch: { e: 11, n: 12, w: 13, s: 14 }
|
||||||
|
- offset: [1, 0.5]
|
||||||
|
switch: { w: 56, n: 57, e: 58, s: 59 }
|
||||||
|
- row:
|
||||||
|
- offset: [4.5, -0.25]
|
||||||
|
switch: { e: 6, n: 7, w: 8, s: 9 }
|
||||||
|
- offset: [3, -0.25]
|
||||||
|
switch: { w: 51, n: 52, e: 53, s: 54 }
|
||||||
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 |
38
src/lib/assets/random-tips/en.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[
|
||||||
|
"You can use DUP+i to create chords on the fly in any text box",
|
||||||
|
"This site is open source! Check out the full source code on GitHub in the bottom left",
|
||||||
|
"Two letter chords can be activated accidentally in chentry. Be cautious around them",
|
||||||
|
"More inputs in a chord increase the tolerance, making them easier to activate",
|
||||||
|
"The maximum number of outputs in a chord is 256",
|
||||||
|
"You can create backups of your device on the top right",
|
||||||
|
"For programming you should set your auto-delete timeout to about 200ms",
|
||||||
|
"Large parts of this site were written on a CC1",
|
||||||
|
"I use VIM btw...",
|
||||||
|
"I use NixOS btw...",
|
||||||
|
"You can hold shift on the undo button to undo all changes",
|
||||||
|
"GTM stands for Generative Text Menu and can be used to change your device's settings anywhere",
|
||||||
|
"Ambidextrous Throwover (aka Mirror Mode) is a mode designed for one-handed use",
|
||||||
|
"Chentry stands for character entry, or typing letter by letter on a chording enabled device",
|
||||||
|
"Chord modifiers are hard-coded (as of now) and can be used in the English language to add conjugations and more",
|
||||||
|
"You can use 'cursor warping' by adding arrow key actions to a chord, for example to chord '()' with the cursor in the middle of the brackets",
|
||||||
|
"An arpeggiate is a single key press that modifies the preceding chord. Modifiers can be arpeggiated",
|
||||||
|
"Some actions are marked as a 'macro', which means the output is generated by a key sequence rather than a pure key press. Be cautious with those, as they can affect other keys when held together!",
|
||||||
|
"Spurring is a chording only mode which is more advanced, but can greatly improve typing speed when mastered",
|
||||||
|
"The forced chord phenomenon is when typing a word character by character starts to feel unnatural",
|
||||||
|
"Don't be afraid to delete chords you keep getting wrong",
|
||||||
|
"Most people find it easier to start their chord library from scratch rather than learning someone else's",
|
||||||
|
"A common techinque to deal with conflicts is to add DUP or the same key mirrored on the other hand",
|
||||||
|
"A longer chord is not always more difficult",
|
||||||
|
"Riley Keen made headlines when his Monkeytype score of 500WPM using a CC1 got him banned off the site",
|
||||||
|
"A 3d press refers to pressing down into a 5-way switch",
|
||||||
|
"The serial communication protocol used by CCOS is documented on docs.charachorder.com",
|
||||||
|
"The 'CCOS is ready' message can be turned off in the settings",
|
||||||
|
"Most people using the CC1 don't change the a-z key layout, as further modification provides very little benefit",
|
||||||
|
"Using VIM on the default CC1 a-z layout is perfectly doable, it's just a matter of getting used to it",
|
||||||
|
"You can use Nexus to track words you might want to add to your chord library",
|
||||||
|
"The CC1 default layout was 80% science, 20% art",
|
||||||
|
"There is little to no reason to use hjkl in VIM on a CC1 since the arrows keys are so close already",
|
||||||
|
"The device manager automatically creates a backup for you when you reboot your device into the bootloader",
|
||||||
|
"You can use \"compound\", \"macro\", \"suffix\" and \"cursor warp\" in the chord search to find specific types of chords",
|
||||||
|
"You can search for chord inputs by using a leading \"+\", for example \"+a +DUP\" will show all chords with inputs that contain both a and DUP"
|
||||||
|
]
|
||||||
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
|
||||||
197
src/lib/backup/backup.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import type {
|
||||||
|
CharaBackupFile,
|
||||||
|
CharaChordFile,
|
||||||
|
CharaFile,
|
||||||
|
CharaLayoutFile,
|
||||||
|
CharaSettingsFile,
|
||||||
|
} from "$lib/share/chara-file.js";
|
||||||
|
import type { Change } from "$lib/undo-redo.js";
|
||||||
|
import {
|
||||||
|
changes,
|
||||||
|
ChangeType,
|
||||||
|
chords,
|
||||||
|
layout,
|
||||||
|
settings,
|
||||||
|
} from "$lib/undo-redo.js";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import { serialPort } from "../serial/connection";
|
||||||
|
import { csvLayoutToJson, isCsvLayout } from "$lib/backup/compat/legacy-layout";
|
||||||
|
import { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
|
||||||
|
|
||||||
|
export function downloadFile<T extends CharaFile<string>>(contents: T) {
|
||||||
|
const downloadUrl = URL.createObjectURL(
|
||||||
|
new Blob([JSON.stringify(contents)], { type: "application/json" }),
|
||||||
|
);
|
||||||
|
const element = document.createElement("a");
|
||||||
|
element.setAttribute(
|
||||||
|
"download",
|
||||||
|
`${contents.type}-${
|
||||||
|
get(serialPort)?.device
|
||||||
|
}-${new Date().toISOString()}.json`,
|
||||||
|
);
|
||||||
|
element.href = downloadUrl;
|
||||||
|
element.setAttribute("target", "_blank");
|
||||||
|
element.click();
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadBackup() {
|
||||||
|
downloadFile<CharaBackupFile>({
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "backup",
|
||||||
|
history: [
|
||||||
|
[createChordBackup(), createLayoutBackup(), createSettingsBackup()],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLayoutBackup(): CharaLayoutFile {
|
||||||
|
return {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "layout",
|
||||||
|
device: get(serialPort)?.device,
|
||||||
|
layout: get(layout).map((it) => it.map((it) => it.action)) as [
|
||||||
|
number[],
|
||||||
|
number[],
|
||||||
|
number[],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChordBackup(): CharaChordFile {
|
||||||
|
return {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "chords",
|
||||||
|
chords: get(chords).map((it) => [it.actions, it.phrase]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSettingsBackup(): CharaSettingsFile {
|
||||||
|
return {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "settings",
|
||||||
|
settings: get(settings).map((it) => it.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreBackup(event: Event) {
|
||||||
|
const input = (event.target as HTMLInputElement).files![0];
|
||||||
|
if (!input) return;
|
||||||
|
const text = await input.text();
|
||||||
|
if (input.name.endsWith(".json")) {
|
||||||
|
restoreFromFile(JSON.parse(text));
|
||||||
|
} else if (isCsvLayout(text)) {
|
||||||
|
restoreFromFile(csvLayoutToJson(text));
|
||||||
|
} else if (isCsvChords(text)) {
|
||||||
|
restoreFromFile(csvChordsToJson(text));
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreFromFile(
|
||||||
|
file: CharaBackupFile | CharaSettingsFile | CharaLayoutFile | CharaChordFile,
|
||||||
|
) {
|
||||||
|
if (file.charaVersion !== 1) throw new Error("Incompatible backup");
|
||||||
|
switch (file.type) {
|
||||||
|
case "backup": {
|
||||||
|
const recent = file.history[0];
|
||||||
|
if (!recent) return;
|
||||||
|
let backupDevice = recent[1].device;
|
||||||
|
if (backupDevice === "TWO") backupDevice = "ONE";
|
||||||
|
let currentDevice = get(serialPort)?.device;
|
||||||
|
if (currentDevice === "TWO") currentDevice = "ONE";
|
||||||
|
|
||||||
|
if (backupDevice !== currentDevice) {
|
||||||
|
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
@@ -0,0 +1,26 @@
|
|||||||
|
e + b + a,babe
|
||||||
|
e + c + b,because
|
||||||
|
f + e + c + a,face
|
||||||
|
h + e + c + a,each
|
||||||
|
i + d + ',I'd
|
||||||
|
i + g + b,big
|
||||||
|
i + g + e,give
|
||||||
|
k + b + a,back
|
||||||
|
k + e + a,take
|
||||||
|
l + e + a,late
|
||||||
|
l + e + d + a,lead
|
||||||
|
l + f + e,feel
|
||||||
|
l + g + e + a,large
|
||||||
|
l + h + e,help
|
||||||
|
l + i + a,Lia
|
||||||
|
l + i + f,fill
|
||||||
|
l + i + f + e,life
|
||||||
|
l + i + g + b + a,gitlab
|
||||||
|
l + k + i + e,like
|
||||||
|
m + e + a,make
|
||||||
|
m + i + ',I'm
|
||||||
|
n + c + a,can
|
||||||
|
n + d + a,and
|
||||||
|
n + e + b,been
|
||||||
|
n + e + b + a,enable
|
||||||
|
n + e + d,end
|
||||||
|
31
src/lib/backup/compat/legacy-chords.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { KEYMAP_IDS } from "$lib/serial/keymap-codes";
|
||||||
|
import type { CharaChordFile } from "$lib/share/chara-file";
|
||||||
|
|
||||||
|
const SPECIAL_KEYS = new Map<string, string>([[" ", "SPACE"]]);
|
||||||
|
|
||||||
|
export function csvChordsToJson(csv: string): CharaChordFile {
|
||||||
|
return {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "chords",
|
||||||
|
chords: csv
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
const [input, output] = line.split(/,(?=[^,]*$)/, 2);
|
||||||
|
return [
|
||||||
|
input!
|
||||||
|
.split("+")
|
||||||
|
.map((it) => KEYMAP_IDS.get(it.trim())?.code ?? 0)
|
||||||
|
.sort((a, b) => a - b),
|
||||||
|
output!
|
||||||
|
.trim()
|
||||||
|
.split("")
|
||||||
|
.map((it) => KEYMAP_IDS.get(SPECIAL_KEYS.get(it) ?? it)?.code ?? 0),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCsvChords(csv: string): boolean {
|
||||||
|
return /^([^+]+( *\+ *[^+]+)* *, *[^+, ]+ *(\n|(?=$)))+$/.test(csv);
|
||||||
|
}
|
||||||
27
src/lib/backup/compat/legacy-layout-converted.sample.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"charaVersion": 1,
|
||||||
|
"type": "layout",
|
||||||
|
"device": "one",
|
||||||
|
"layout": [
|
||||||
|
[
|
||||||
|
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262,
|
||||||
|
288, 277, 298, 307, 264, 287, 268, 332, 311, 274, 286, 308, 329, 310, 280,
|
||||||
|
358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315, 314, 317, 316,
|
||||||
|
312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263,
|
||||||
|
293, 260, 296, 544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278,
|
||||||
|
357, 516, 519, 517, 518, 327, 336, 338, 335, 337, 328, 325, 322, 323, 324
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
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
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import legacyLayout from "./legacy-layout.sample.csv?raw";
|
||||||
|
import legacyLayoutConverted from "./legacy-layout-converted.sample.json";
|
||||||
|
import { csvLayoutToJson, isCsvLayout } from "./legacy-layout";
|
||||||
|
|
||||||
|
describe("legacy layout", () => {
|
||||||
|
it("should detect a legacy layout", () => {
|
||||||
|
expect(isCsvLayout(legacyLayout)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not detect chord maps as layouts", () => {
|
||||||
|
expect(isCsvLayout("e + h + t,the")).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert legacy layouts", () => {
|
||||||
|
expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted);
|
||||||
|
});
|
||||||
|
});
|
||||||
28
src/lib/backup/compat/legacy-layout.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { CharaLayoutFile } from "$lib/share/chara-file";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a legacy CSV-based layout to the modern JSON-based format
|
||||||
|
*/
|
||||||
|
export function csvLayoutToJson(
|
||||||
|
csv: string,
|
||||||
|
device: CharaLayoutFile["device"] = "one",
|
||||||
|
): CharaLayoutFile {
|
||||||
|
const layout: CharaLayoutFile = {
|
||||||
|
charaVersion: 1,
|
||||||
|
type: "layout",
|
||||||
|
device,
|
||||||
|
layout: [[], [], []],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const layer of csv.trim().split("\n")) {
|
||||||
|
const [layerId, key, action] = layer.substring(1).split(",").map(Number);
|
||||||
|
|
||||||
|
layout.layout[Number(layerId) - 1]![Number(key)] = Number(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCsvLayout(csv: string): boolean {
|
||||||
|
return /^(A[123],\d+,\d+\n?)+$/.test(csv);
|
||||||
|
}
|
||||||
140
src/lib/charrecorder/CharRecorder.svelte
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { ReplayPlayer } from "./core/player.js";
|
||||||
|
import { ReplayStepper } from "./core/step.js";
|
||||||
|
import type { Replay } from "./core/types.js";
|
||||||
|
import { TextRenderer } from "./renderer/renderer.js";
|
||||||
|
import { setContext, type Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
replay,
|
||||||
|
cursor = false,
|
||||||
|
keys = false,
|
||||||
|
children,
|
||||||
|
ondone,
|
||||||
|
}: {
|
||||||
|
replay: ReplayPlayer | Replay;
|
||||||
|
cursor?: boolean;
|
||||||
|
keys?: boolean;
|
||||||
|
children?: Snippet;
|
||||||
|
ondone?: () => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let replayPlayer: ReplayPlayer | undefined = $state();
|
||||||
|
setContext("replay", {
|
||||||
|
get player() {
|
||||||
|
return replayPlayer;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let finalText = $derived(
|
||||||
|
replay instanceof ReplayPlayer
|
||||||
|
? undefined
|
||||||
|
: new ReplayStepper(replay.keys).text.map((token) => token.text).join(""),
|
||||||
|
);
|
||||||
|
|
||||||
|
let svg: SVGSVGElement | undefined = $state();
|
||||||
|
let text: Text = (browser ? document.createTextNode("") : undefined)!;
|
||||||
|
|
||||||
|
let textRenderer: TextRenderer | undefined = $state();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!textRenderer) return;
|
||||||
|
textRenderer.showCursor = cursor;
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!svg || !text) return;
|
||||||
|
const player =
|
||||||
|
replay instanceof ReplayPlayer ? replay : new ReplayPlayer(replay);
|
||||||
|
replayPlayer = player;
|
||||||
|
|
||||||
|
const renderer = new TextRenderer(svg.parentNode as HTMLElement, svg, text);
|
||||||
|
const apply = () => {
|
||||||
|
text.textContent =
|
||||||
|
finalText ??
|
||||||
|
(player.stepper.text.map((token) => token.text).join("") || "n");
|
||||||
|
renderer.text = player.stepper.text;
|
||||||
|
renderer.cursor = player.stepper.cursor;
|
||||||
|
if (keys) {
|
||||||
|
renderer.held = player.stepper.held;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const unsubscribePlayer = player.subscribe(apply);
|
||||||
|
textRenderer = renderer;
|
||||||
|
|
||||||
|
player.onDone = ondone;
|
||||||
|
player.start();
|
||||||
|
apply();
|
||||||
|
setTimeout(() => {
|
||||||
|
renderer.animated = true;
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsubscribePlayer();
|
||||||
|
player?.destroy();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function innerText(node: HTMLElement, text: Text) {
|
||||||
|
node.appendChild(text);
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
text.remove();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key replay}
|
||||||
|
<svg bind:this={svg}></svg>
|
||||||
|
{#if browser}
|
||||||
|
<span use:innerText={text}></span>
|
||||||
|
{:else if !(replay instanceof ReplayPlayer)}
|
||||||
|
{finalText}
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(*):has(svg) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
opacity: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg > :global(text) {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
fill: currentColor;
|
||||||
|
dominant-baseline: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg > :global(text[incorrect]) {
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg > :global(rect) {
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg > :global(.animated) {
|
||||||
|
transition: transform 100ms ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
src/lib/charrecorder/ChordHud.svelte
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly, scale } from "svelte/transition";
|
||||||
|
import { KBD_ICONS } from "./renderer/kbd-icon.js";
|
||||||
|
import { expoOut } from "svelte/easing";
|
||||||
|
import type { InferredChord } from "./core/types.js";
|
||||||
|
|
||||||
|
let { chords }: { chords: InferredChord[] } = $props();
|
||||||
|
|
||||||
|
function getPercent(
|
||||||
|
deviation: number,
|
||||||
|
inputCount: number,
|
||||||
|
perfect: number,
|
||||||
|
fail: number,
|
||||||
|
) {
|
||||||
|
const failAdjusted = fail * inputCount;
|
||||||
|
const perfectAdjusted = perfect * inputCount;
|
||||||
|
return Math.min(
|
||||||
|
1,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.max(0, deviation - perfectAdjusted) /
|
||||||
|
(failAdjusted - perfectAdjusted),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColor(percent: number, alpha = 1) {
|
||||||
|
return `hsl(${(1 - percent) * 120}deg 50% 50% / ${alpha})`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
{#each chords as { input, id, deviation }, i (id)}
|
||||||
|
{@const a = getPercent(deviation[0], input.length, 10, 25)}
|
||||||
|
{@const b = getPercent(deviation[1], input.length, 10, 18)}
|
||||||
|
{@const max = Math.max(a, b)}
|
||||||
|
<div
|
||||||
|
class="chord"
|
||||||
|
out:fly={{ x: -100 }}
|
||||||
|
style:translate="calc(-{(chords.length - i - 1) * 5}em - 50%) 0"
|
||||||
|
style:scale={1 - (chords.length - i) / 6}
|
||||||
|
style:opacity={1 - (chords.length - i - 1) / 6}
|
||||||
|
title="Press: {deviation[0]}ms, Release: {deviation[1]}ms"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rating"
|
||||||
|
style:color={getColor(max)}
|
||||||
|
style:text-shadow="0 0 {Math.round((1 - max) * 10)}px {getColor(
|
||||||
|
max,
|
||||||
|
0.6,
|
||||||
|
)}"
|
||||||
|
in:scale={{
|
||||||
|
start: 1.5 + 1.2 * (1 - max),
|
||||||
|
easing: expoOut,
|
||||||
|
duration: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if max === 1}
|
||||||
|
Close
|
||||||
|
{:else if max > 0.5}
|
||||||
|
Okay
|
||||||
|
{:else if max > 0}
|
||||||
|
Good
|
||||||
|
{:else}
|
||||||
|
Perfect
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
in:fly={{ y: 20, easing: expoOut, duration: 1000 }}
|
||||||
|
class="tile"
|
||||||
|
style:background="linear-gradient(to right, {getColor(a)}, {getColor(
|
||||||
|
b,
|
||||||
|
)})"
|
||||||
|
></div>
|
||||||
|
<div in:fly={{ y: 60, easing: expoOut, duration: 1000 }}>
|
||||||
|
{#each input as token}
|
||||||
|
<kbd>{KBD_ICONS.get(token.code)}</kbd>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
position: relative;
|
||||||
|
margin: 1em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: grid;
|
||||||
|
height: 3em;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: 100%;
|
||||||
|
height: 0.2em;
|
||||||
|
border-radius: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-size: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd + kbd {
|
||||||
|
margin-inline-start: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord {
|
||||||
|
will-change: transform, opacity, scale;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-inline-end: 1em;
|
||||||
|
padding-inline: 0.1em;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
translate 0.3s ease,
|
||||||
|
scale 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
src/lib/charrecorder/TrackChords.svelte
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import type { InferredChord } from "./core/types.js";
|
||||||
|
import { ChordsReplayPlugin } from "./core/plugins/chords.js";
|
||||||
|
import type { ReplayPlayer } from "./core/player.js";
|
||||||
|
|
||||||
|
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||||
|
|
||||||
|
let {
|
||||||
|
chords = $bindable([]),
|
||||||
|
count = 1,
|
||||||
|
}: {
|
||||||
|
chords: InferredChord[];
|
||||||
|
count?: number;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
$effect(() => {
|
||||||
|
if (!player.player) return;
|
||||||
|
const tracker = new ChordsReplayPlugin();
|
||||||
|
tracker.register(player.player);
|
||||||
|
const unsubscribe = tracker.subscribe((value) => {
|
||||||
|
chords = value.slice(-count);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
20
src/lib/charrecorder/TrackRollingWpm.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { RollingWpmReplayPlugin } from "./core/plugins/rolling-wpm";
|
||||||
|
import type { ReplayPlayer } from "./core/player";
|
||||||
|
|
||||||
|
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||||
|
|
||||||
|
let { wpm = $bindable(0) } = $props();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!player.player) return;
|
||||||
|
const tracker = new RollingWpmReplayPlugin();
|
||||||
|
tracker.register(player.player);
|
||||||
|
const unsubscribe = tracker.subscribe((value) => {
|
||||||
|
wpm = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
146
src/lib/charrecorder/core/player.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { ReplayStepper } from "./step";
|
||||||
|
import type { ReplayPlugin, Replay, TextToken } from "./types";
|
||||||
|
|
||||||
|
export const ROBOT_THRESHOLD = 20;
|
||||||
|
|
||||||
|
export class ReplayPlayer {
|
||||||
|
stepper = new ReplayStepper();
|
||||||
|
|
||||||
|
private replayCursor = 0;
|
||||||
|
|
||||||
|
private releaseAt = new Map<string, number>();
|
||||||
|
|
||||||
|
startTime = performance.now();
|
||||||
|
|
||||||
|
private animationFrameId: number | null = null;
|
||||||
|
|
||||||
|
timescale = 1;
|
||||||
|
|
||||||
|
private subscribers = new Set<(value: TextToken | undefined) => void>();
|
||||||
|
|
||||||
|
onDone?: () => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly replay: Replay,
|
||||||
|
plugins: ReplayPlugin[] = [],
|
||||||
|
) {
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
plugin.register(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('./types').StoreContract<import('./types').TextToken | undefined>['subscribe']} */
|
||||||
|
subscribe(subscription: (value: TextToken | undefined) => void) {
|
||||||
|
this.subscribers.add(subscription);
|
||||||
|
return () => this.subscribers.delete(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLoop() {
|
||||||
|
if (
|
||||||
|
this.replayCursor >= this.replay.keys.length &&
|
||||||
|
this.releaseAt.size === 0
|
||||||
|
) {
|
||||||
|
if (this.onDone) {
|
||||||
|
this.onDone();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = performance.now() - this.startTime;
|
||||||
|
|
||||||
|
while (
|
||||||
|
this.replayCursor < this.replay.keys.length &&
|
||||||
|
this.replay.keys[this.replayCursor]![2] * this.timescale -
|
||||||
|
this.replay.start <=
|
||||||
|
now
|
||||||
|
) {
|
||||||
|
const [key, code, at, duration] = this.replay.keys[this.replayCursor++]!;
|
||||||
|
this.stepper.held.set(code, duration > ROBOT_THRESHOLD);
|
||||||
|
this.releaseAt.set(code, now + duration * this.timescale);
|
||||||
|
|
||||||
|
const token = this.stepper.step(key, code, at, duration);
|
||||||
|
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, releaseAt] of this.releaseAt) {
|
||||||
|
if (releaseAt > now) continue;
|
||||||
|
this.stepper.held.delete(key);
|
||||||
|
this.releaseAt.delete(key);
|
||||||
|
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.updateLoop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
playLiveEvent(key: string, code: string): (duration: number) => void {
|
||||||
|
this.replay.start = this.startTime;
|
||||||
|
const at = performance.now();
|
||||||
|
this.stepper.held.set(code, false);
|
||||||
|
|
||||||
|
const token = this.stepper.step(key, code, at) ?? {
|
||||||
|
text: key,
|
||||||
|
code,
|
||||||
|
stamp: at,
|
||||||
|
correct: true,
|
||||||
|
source: "robot",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
token.source = "human";
|
||||||
|
this.stepper.held.set(code, true);
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(undefined);
|
||||||
|
}
|
||||||
|
}, ROBOT_THRESHOLD);
|
||||||
|
|
||||||
|
return (duration) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (token) {
|
||||||
|
// TODO: will this cause performance issues with long text?
|
||||||
|
const index = this.stepper.text.indexOf(token);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.stepper.text[index]!.duration = duration;
|
||||||
|
this.stepper.text[index]!.source =
|
||||||
|
duration < ROBOT_THRESHOLD ? "robot" : "human";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.stepper.held.delete(code);
|
||||||
|
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
start(delay = 200): this {
|
||||||
|
this.replayCursor = 0;
|
||||||
|
this.stepper = new ReplayStepper([], this.replay.challenge);
|
||||||
|
if (this.replay.keys.length === 0) {
|
||||||
|
if (this.onDone) {
|
||||||
|
this.onDone();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.startTime = performance.now();
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.updateLoop.bind(this));
|
||||||
|
}, delay);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.animationFrameId) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/lib/charrecorder/core/plugins/chords.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { ReplayPlayer, ROBOT_THRESHOLD } from "../player";
|
||||||
|
import type {
|
||||||
|
StoreContract,
|
||||||
|
ReplayPlugin,
|
||||||
|
InferredChord,
|
||||||
|
TextToken,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
function isValid(human: TextToken[], robot: TextToken[]) {
|
||||||
|
return human.length > 1 && human.length <= 10 && robot.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChordsReplayPlugin
|
||||||
|
implements StoreContract<InferredChord[]>, ReplayPlugin
|
||||||
|
{
|
||||||
|
private readonly subscribers = new Set<(value: InferredChord[]) => void>();
|
||||||
|
|
||||||
|
private readonly chords: InferredChord[] = [];
|
||||||
|
|
||||||
|
private tokens: TextToken[] = [];
|
||||||
|
|
||||||
|
private timeout: Parameters<typeof clearTimeout>[0] = NaN;
|
||||||
|
|
||||||
|
private infer(human: TextToken[], robo: TextToken[]) {
|
||||||
|
const output = robo
|
||||||
|
.filter((token) => token.text.length === 1)
|
||||||
|
.map((token) => token.text)
|
||||||
|
.join("");
|
||||||
|
this.chords.push({
|
||||||
|
id: human.reduce((acc, curr) => Math.max(acc, curr.stamp), 0),
|
||||||
|
input: human,
|
||||||
|
output,
|
||||||
|
deviation: [
|
||||||
|
human.reduce((acc, curr) => Math.max(acc, curr.stamp), 0) -
|
||||||
|
human.reduce((acc, curr) => Math.min(acc, curr.stamp), Infinity),
|
||||||
|
human.reduce(
|
||||||
|
(acc, curr) => Math.max(acc, curr.stamp + (curr.duration ?? 0)),
|
||||||
|
0,
|
||||||
|
) -
|
||||||
|
human.reduce(
|
||||||
|
(acc, curr) => Math.min(acc, curr.stamp + (curr.duration ?? 0)),
|
||||||
|
Infinity,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(this.chords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register(replay: ReplayPlayer) {
|
||||||
|
replay.subscribe((token) => {
|
||||||
|
if (token) {
|
||||||
|
this.tokens.push(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let last = NaN;
|
||||||
|
let roboStart = NaN;
|
||||||
|
let roboEnd = NaN;
|
||||||
|
for (let i = 0; i < this.tokens.length; i++) {
|
||||||
|
const token = this.tokens[i]!;
|
||||||
|
if (!token.duration || !token.source) break;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Number.isNaN(roboStart) &&
|
||||||
|
token.source === "human" &&
|
||||||
|
token.stamp > last
|
||||||
|
) {
|
||||||
|
this.tokens = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(last) || token.stamp + token.duration > last) {
|
||||||
|
last = token.stamp + token.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(roboStart) && token.source === "robot") {
|
||||||
|
roboStart = i;
|
||||||
|
} else if (!Number.isNaN(roboStart) && token.source === "human") {
|
||||||
|
roboEnd = i;
|
||||||
|
const human = this.tokens.splice(0, roboStart);
|
||||||
|
const robot = this.tokens.splice(0, roboEnd - roboStart);
|
||||||
|
if (isValid(human, robot)) {
|
||||||
|
this.infer(human, robot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(this.tokens);
|
||||||
|
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
if (replay.stepper.held.size === 0) {
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
if (this.tokens.length > 0) {
|
||||||
|
const human = this.tokens.splice(
|
||||||
|
0,
|
||||||
|
this.tokens.findIndex((it) => it.source === "robot"),
|
||||||
|
);
|
||||||
|
const robot = this.tokens.splice(0, this.tokens.length);
|
||||||
|
if (isValid(human, robot)) {
|
||||||
|
this.infer(human, robot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, ROBOT_THRESHOLD);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(subscription: (value: InferredChord[]) => void) {
|
||||||
|
this.subscribers.add(subscription);
|
||||||
|
return () => this.subscribers.delete(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/lib/charrecorder/core/plugins/meta.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ReplayPlayer, ROBOT_THRESHOLD } from "../player";
|
||||||
|
import type { GraphData, ReplayPlugin, StoreContract } from "../types";
|
||||||
|
|
||||||
|
export class MetaReplayPlugin
|
||||||
|
implements StoreContract<GraphData>, ReplayPlugin
|
||||||
|
{
|
||||||
|
private subscribers = new Set<(value: GraphData) => void>();
|
||||||
|
|
||||||
|
private graphData: GraphData = { min: [0, 0], max: [0, 0], tokens: [] };
|
||||||
|
|
||||||
|
private liveHeldRoboFilter = new Set<string>();
|
||||||
|
|
||||||
|
register(replay: ReplayPlayer) {
|
||||||
|
replay.subscribe((token) => {
|
||||||
|
if (!token) return;
|
||||||
|
const lastHeld = this.graphData.tokens
|
||||||
|
.at(-1)
|
||||||
|
?.reduce(
|
||||||
|
(acc, curr) => Math.max(acc, curr.stamp + (curr.duration ?? 0)),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
lastHeld &&
|
||||||
|
(lastHeld === -1 || lastHeld > token.stamp + (token.duration ?? 0))
|
||||||
|
) {
|
||||||
|
this.graphData.tokens.at(-1)!.push(token);
|
||||||
|
} else {
|
||||||
|
this.graphData.tokens.push([token]);
|
||||||
|
}
|
||||||
|
if (this.graphData.tokens.length === 1) {
|
||||||
|
this.graphData.min = [token.stamp, 0];
|
||||||
|
}
|
||||||
|
this.graphData.max = [
|
||||||
|
this.graphData.tokens
|
||||||
|
.at(-1)!
|
||||||
|
.reduce(
|
||||||
|
(acc, { stamp, duration }) =>
|
||||||
|
Math.max(acc, stamp + (duration ?? 0)),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
Math.max(this.graphData.max[1], this.graphData.tokens.at(-1)!.length),
|
||||||
|
];
|
||||||
|
|
||||||
|
this.liveHeldRoboFilter.add(token.code);
|
||||||
|
|
||||||
|
if (token.duration === undefined) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.liveHeldRoboFilter.has(token.code)) {
|
||||||
|
token.source = "human";
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(this.graphData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, ROBOT_THRESHOLD);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.liveHeldRoboFilter.delete(token.code);
|
||||||
|
}, token.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(this.graphData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(subscription: (value: GraphData) => void) {
|
||||||
|
this.subscribers.add(subscription);
|
||||||
|
return () => this.subscribers.delete(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/lib/charrecorder/core/plugins/rolling-wpm.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { ReplayPlayer } from "../player";
|
||||||
|
import type { ReplayPlugin, StoreContract } from "../types";
|
||||||
|
import { avgWordLength } from "./wpm";
|
||||||
|
|
||||||
|
export class RollingWpmReplayPlugin
|
||||||
|
implements StoreContract<number>, ReplayPlugin
|
||||||
|
{
|
||||||
|
subscribers = new Set<(value: number) => void>();
|
||||||
|
|
||||||
|
register(replay: ReplayPlayer) {
|
||||||
|
replay.subscribe(() => {
|
||||||
|
if (this.subscribers.size === 0) return;
|
||||||
|
let i = 0;
|
||||||
|
const index = Math.max(
|
||||||
|
0,
|
||||||
|
replay.stepper.text.findLastIndex((char) => {
|
||||||
|
if (char.source === "ghost") return false;
|
||||||
|
if (char.text === " " && i < 10) {
|
||||||
|
i++;
|
||||||
|
} else if (char.text === " ") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const length =
|
||||||
|
replay.stepper.text.length - replay.stepper.ghostCount - index;
|
||||||
|
const msPerChar =
|
||||||
|
((replay.stepper.text[
|
||||||
|
replay.stepper.text.length - replay.stepper.ghostCount - 1
|
||||||
|
]?.stamp ?? 0) -
|
||||||
|
(replay.stepper.text[index]?.stamp ?? 0)) /
|
||||||
|
length;
|
||||||
|
|
||||||
|
const value = 60_000 / (msPerChar * avgWordLength);
|
||||||
|
if (Number.isFinite(value)) {
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(subscription: (value: number) => void) {
|
||||||
|
this.subscribers.add(subscription);
|
||||||
|
return () => this.subscribers.delete(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/lib/charrecorder/core/plugins/wpm.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ReplayPlayer } from "../player";
|
||||||
|
import type { ReplayPlugin, StoreContract } from "../types";
|
||||||
|
|
||||||
|
export const avgWordLength = 5;
|
||||||
|
|
||||||
|
export class WpmReplayPlugin implements StoreContract<number>, ReplayPlugin {
|
||||||
|
private subscribers = new Set<(value: number) => void>();
|
||||||
|
|
||||||
|
register(replay: ReplayPlayer) {
|
||||||
|
replay.subscribe(() => {
|
||||||
|
if (this.subscribers.size === 0) return;
|
||||||
|
const msPerChar =
|
||||||
|
((replay.stepper.text.at(-1)?.stamp ?? 0) - replay.startTime) /
|
||||||
|
replay.stepper.text.length;
|
||||||
|
|
||||||
|
const value = 60_000 / (msPerChar * avgWordLength);
|
||||||
|
for (const subscription of this.subscribers) {
|
||||||
|
subscription(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
subscribe(subscription: (value: number) => void) {
|
||||||
|
this.subscribers.add(subscription);
|
||||||
|
return () => this.subscribers.delete(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/lib/charrecorder/core/recorder.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { ReplayPlayer } from "./player.js";
|
||||||
|
import type { Replay, ReplayEvent, TransmittableKeyEvent } from "./types.js";
|
||||||
|
|
||||||
|
function maybeRound<T>(value: T, round: boolean): T {
|
||||||
|
return typeof value === "number" && round ? (Math.round(value) as T) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReplayRecorder {
|
||||||
|
private held = new Map<string, [string, number]>();
|
||||||
|
|
||||||
|
private heldHandles = new Map<
|
||||||
|
string,
|
||||||
|
ReturnType<ReplayPlayer["playLiveEvent"]>
|
||||||
|
>();
|
||||||
|
|
||||||
|
replay: ReplayEvent[] = [];
|
||||||
|
|
||||||
|
private start = performance.now();
|
||||||
|
|
||||||
|
private isFirstPress = true;
|
||||||
|
|
||||||
|
player: ReplayPlayer;
|
||||||
|
|
||||||
|
constructor(challenge?: Replay["challenge"]) {
|
||||||
|
this.player = new ReplayPlayer({
|
||||||
|
start: this.start,
|
||||||
|
finish: this.start,
|
||||||
|
keys: [],
|
||||||
|
challenge,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next(event: TransmittableKeyEvent) {
|
||||||
|
if (this.isFirstPress) {
|
||||||
|
this.player.startTime = event.timeStamp;
|
||||||
|
this.isFirstPress = false;
|
||||||
|
}
|
||||||
|
this.player.replay.finish = event.timeStamp;
|
||||||
|
if (event.type === "keydown") {
|
||||||
|
this.held.set(event.code, [event.key, event.timeStamp]);
|
||||||
|
this.heldHandles.set(
|
||||||
|
event.code,
|
||||||
|
this.player.playLiveEvent(event.key, event.code),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const [key, start] = this.held.get(event.code) ?? ["", 0];
|
||||||
|
const delta = event.timeStamp - start;
|
||||||
|
this.held.delete(event.code);
|
||||||
|
|
||||||
|
const element = Object.freeze([key, event.code, start, delta] as const);
|
||||||
|
this.replay.push(element);
|
||||||
|
this.heldHandles.get(event.code)?.(delta);
|
||||||
|
this.heldHandles.delete(event.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(trim = true, round = true) {
|
||||||
|
return {
|
||||||
|
start: maybeRound(trim ? this.replay[0]?.[2] : this.start, round),
|
||||||
|
finish: maybeRound(
|
||||||
|
trim
|
||||||
|
? Math.max(...this.replay.map((it) => it[2] + it[3]))
|
||||||
|
: performance.now(),
|
||||||
|
round,
|
||||||
|
),
|
||||||
|
keys: this.replay
|
||||||
|
.map(
|
||||||
|
([key, code, at, duration]) =>
|
||||||
|
[
|
||||||
|
key,
|
||||||
|
code,
|
||||||
|
maybeRound(at, round),
|
||||||
|
maybeRound(duration, round),
|
||||||
|
] as const,
|
||||||
|
)
|
||||||
|
.sort((a, b) => a[2] - b[2]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/lib/charrecorder/core/step.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { ROBOT_THRESHOLD } from "./player";
|
||||||
|
import type { LiveReplayEvent, ReplayEvent, TextToken } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the "heart" of the player logic
|
||||||
|
*/
|
||||||
|
export class ReplayStepper {
|
||||||
|
held = new Map<string, boolean>();
|
||||||
|
|
||||||
|
text: TextToken[];
|
||||||
|
|
||||||
|
cursor = 0;
|
||||||
|
|
||||||
|
challenge: TextToken[];
|
||||||
|
|
||||||
|
ghostCount: number;
|
||||||
|
|
||||||
|
mistakeCount = 0;
|
||||||
|
|
||||||
|
constructor(initialReplay: ReplayEvent[] = [], challenge = "") {
|
||||||
|
this.challenge = challenge.split("").map((text) => ({
|
||||||
|
stamp: 0,
|
||||||
|
duration: 0,
|
||||||
|
code: "",
|
||||||
|
text,
|
||||||
|
source: "ghost",
|
||||||
|
correct: true,
|
||||||
|
}));
|
||||||
|
this.text = [...this.challenge];
|
||||||
|
this.ghostCount = this.challenge.length;
|
||||||
|
for (const key of initialReplay) {
|
||||||
|
this.step(...key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step(
|
||||||
|
...[output, code, at, duration]: ReplayEvent | LiveReplayEvent
|
||||||
|
): TextToken | undefined {
|
||||||
|
let token: TextToken | undefined = undefined;
|
||||||
|
if (output === "Backspace") {
|
||||||
|
if (this.held.has("ControlLeft") || this.held.has("ControlRight")) {
|
||||||
|
let wordIndex = 0;
|
||||||
|
for (let i = this.cursor - 1; i >= 0; i--) {
|
||||||
|
if (/\w+/.test(/** @type {TextToken} */ this.text[i]!.text)) {
|
||||||
|
wordIndex = i;
|
||||||
|
} else if (wordIndex !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.text.splice(wordIndex, this.cursor - wordIndex);
|
||||||
|
} else if (this.cursor !== 0) {
|
||||||
|
this.text.splice(this.cursor - 1, 1);
|
||||||
|
}
|
||||||
|
this.cursor = Math.min(
|
||||||
|
this.cursor,
|
||||||
|
this.text.length - this.ghostCount + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (output.length === 1) {
|
||||||
|
token = {
|
||||||
|
stamp: at,
|
||||||
|
duration,
|
||||||
|
code,
|
||||||
|
text: output,
|
||||||
|
source:
|
||||||
|
duration === undefined
|
||||||
|
? undefined
|
||||||
|
: duration < ROBOT_THRESHOLD
|
||||||
|
? "robot"
|
||||||
|
: "human",
|
||||||
|
correct: true,
|
||||||
|
};
|
||||||
|
this.text.splice(this.cursor, 0, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === "ArrowLeft" || code === "Backspace") {
|
||||||
|
this.cursor = Math.max(this.cursor - 1, 0);
|
||||||
|
}
|
||||||
|
if (code === "ArrowRight" || output.length === 1) {
|
||||||
|
this.cursor = Math.min(
|
||||||
|
this.cursor + 1,
|
||||||
|
this.text.length - this.ghostCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === "Enter") {
|
||||||
|
token = {
|
||||||
|
stamp: at,
|
||||||
|
code,
|
||||||
|
duration,
|
||||||
|
text: "\n",
|
||||||
|
source:
|
||||||
|
duration === undefined
|
||||||
|
? undefined
|
||||||
|
: duration < ROBOT_THRESHOLD
|
||||||
|
? "robot"
|
||||||
|
: "human",
|
||||||
|
correct: true,
|
||||||
|
};
|
||||||
|
this.text.splice(this.cursor, 0, token);
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.challenge.length > 0) {
|
||||||
|
let challengeIndex = 0;
|
||||||
|
this.mistakeCount = 0;
|
||||||
|
for (let i = 0; i < this.text.length - this.ghostCount; i++) {
|
||||||
|
if (this.text[i]!.text === this.challenge[challengeIndex]?.text) {
|
||||||
|
this.text[i]!.correct = true;
|
||||||
|
} else {
|
||||||
|
this.mistakeCount++;
|
||||||
|
this.text[i]!.correct = false;
|
||||||
|
}
|
||||||
|
challengeIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentGhostCount = this.ghostCount;
|
||||||
|
this.ghostCount = Math.max(0, this.challenge.length - challengeIndex);
|
||||||
|
|
||||||
|
this.text.splice(
|
||||||
|
this.text.length - currentGhostCount,
|
||||||
|
Math.max(0, currentGhostCount - this.ghostCount),
|
||||||
|
...this.challenge.slice(
|
||||||
|
challengeIndex,
|
||||||
|
challengeIndex + Math.max(0, this.ghostCount - currentGhostCount),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/lib/charrecorder/core/types.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { ReplayPlayer } from "./player.js";
|
||||||
|
|
||||||
|
export interface Replay {
|
||||||
|
start: number;
|
||||||
|
finish: number;
|
||||||
|
keys: ReplayEvent[];
|
||||||
|
challenge?: string;
|
||||||
|
}
|
||||||
|
export type LiveReplayEvent = readonly [
|
||||||
|
output: string,
|
||||||
|
code: string,
|
||||||
|
at: number,
|
||||||
|
];
|
||||||
|
export type ReplayEvent = readonly [...LiveReplayEvent, duration: number];
|
||||||
|
|
||||||
|
export interface TextToken {
|
||||||
|
stamp: number;
|
||||||
|
duration?: number;
|
||||||
|
text: string;
|
||||||
|
code: string;
|
||||||
|
source?: "human" | "robot" | "ghost";
|
||||||
|
correct: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphData {
|
||||||
|
min: [number, number];
|
||||||
|
max: [number, number];
|
||||||
|
tokens: TextToken[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplayStepResult {
|
||||||
|
text: TextToken[];
|
||||||
|
cursor: number;
|
||||||
|
challengeCursor: number;
|
||||||
|
token: TextToken | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransmittableKeyEvent = Pick<
|
||||||
|
KeyboardEvent,
|
||||||
|
"timeStamp" | "type" | "code" | "key"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface InferredChord {
|
||||||
|
id: number;
|
||||||
|
input: TextToken[];
|
||||||
|
output: string;
|
||||||
|
deviation: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplayPlugin {
|
||||||
|
register(replay: ReplayPlayer): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreContract<T> {
|
||||||
|
subscribe(subscription: (value: T) => void): () => void;
|
||||||
|
|
||||||
|
set?: (value: T) => void;
|
||||||
|
}
|
||||||
96
src/lib/charrecorder/renderer/kbd-icon.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
export const KBD_ICONS = new Map([
|
||||||
|
["KeyA", "a"],
|
||||||
|
["KeyB", "b"],
|
||||||
|
["KeyC", "c"],
|
||||||
|
["KeyD", "d"],
|
||||||
|
["KeyE", "e"],
|
||||||
|
["KeyF", "f"],
|
||||||
|
["KeyG", "g"],
|
||||||
|
["KeyH", "h"],
|
||||||
|
["KeyI", "i"],
|
||||||
|
["KeyJ", "j"],
|
||||||
|
["KeyK", "k"],
|
||||||
|
["KeyL", "l"],
|
||||||
|
["KeyM", "m"],
|
||||||
|
["KeyN", "n"],
|
||||||
|
["KeyO", "o"],
|
||||||
|
["KeyP", "p"],
|
||||||
|
["KeyQ", "q"],
|
||||||
|
["KeyR", "r"],
|
||||||
|
["KeyS", "s"],
|
||||||
|
["KeyT", "t"],
|
||||||
|
["KeyU", "u"],
|
||||||
|
["KeyV", "v"],
|
||||||
|
["KeyW", "w"],
|
||||||
|
["KeyX", "x"],
|
||||||
|
["KeyY", "y"],
|
||||||
|
["KeyZ", "z"],
|
||||||
|
["Digit0", "0"],
|
||||||
|
["Digit1", "1"],
|
||||||
|
["Digit2", "2"],
|
||||||
|
["Digit3", "3"],
|
||||||
|
["Digit4", "4"],
|
||||||
|
["Digit5", "5"],
|
||||||
|
["Digit6", "6"],
|
||||||
|
["Digit7", "7"],
|
||||||
|
["Digit8", "8"],
|
||||||
|
["Digit9", "9"],
|
||||||
|
["Period", "."],
|
||||||
|
["Comma", ","],
|
||||||
|
["Semicolon", ";"],
|
||||||
|
["Quote", "'"],
|
||||||
|
["BracketLeft", "["],
|
||||||
|
["BracketRight", "]"],
|
||||||
|
["Backslash", "\\"],
|
||||||
|
["Slash", "/"],
|
||||||
|
["Minus", "-"],
|
||||||
|
["Equal", "="],
|
||||||
|
["Backquote", "`"],
|
||||||
|
["IntlBackslash", "¦"],
|
||||||
|
["IntlRo", "ろ"],
|
||||||
|
["IntlYen", "¥"],
|
||||||
|
["IntlHash", "#"],
|
||||||
|
["BracketLeft", "["],
|
||||||
|
["BracketRight", "]"],
|
||||||
|
["NumLock", "⇭"],
|
||||||
|
["ScrollLock", "⇳"],
|
||||||
|
["Backspace", "⌫"],
|
||||||
|
["Delete", "⌦"],
|
||||||
|
["Enter", "↵"],
|
||||||
|
["Space", "␣"],
|
||||||
|
["Tab", "⇥"],
|
||||||
|
["ArrowLeft", "←"],
|
||||||
|
["ArrowRight", "→"],
|
||||||
|
["ArrowUp", "↑"],
|
||||||
|
["ArrowDown", "↓"],
|
||||||
|
["ShiftLeft", "⇧"],
|
||||||
|
["ShiftRight", "⇧"],
|
||||||
|
["ControlLeft", "Ctrl"],
|
||||||
|
["ControlRight", "Ctrl"],
|
||||||
|
["AltLeft", "Alt"],
|
||||||
|
["AltRight", "Alt"],
|
||||||
|
["MetaLeft", "⌘"],
|
||||||
|
["MetaRight", "⌘"],
|
||||||
|
["CapsLock", "⇪"],
|
||||||
|
["Escape", "Esc"],
|
||||||
|
["F1", "F1"],
|
||||||
|
["F2", "F2"],
|
||||||
|
["F3", "F3"],
|
||||||
|
["F4", "F4"],
|
||||||
|
["F5", "F5"],
|
||||||
|
["F6", "F6"],
|
||||||
|
["F7", "F7"],
|
||||||
|
["F8", "F8"],
|
||||||
|
["F9", "F9"],
|
||||||
|
["F10", "F10"],
|
||||||
|
["F11", "F11"],
|
||||||
|
["F12", "F12"],
|
||||||
|
["PrintScreen", "PrtSc"],
|
||||||
|
["Pause", "Pause"],
|
||||||
|
["Insert", "Ins"],
|
||||||
|
["Home", "Home"],
|
||||||
|
["End", "End"],
|
||||||
|
["PageUp", "PgUp"],
|
||||||
|
["PageDown", "PgDn"],
|
||||||
|
["ContextMenu", "Menu"],
|
||||||
|
]);
|
||||||
287
src/lib/charrecorder/renderer/renderer.ts
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
import type { TextToken } from "../core/types";
|
||||||
|
import { KBD_ICONS } from "./kbd-icon";
|
||||||
|
|
||||||
|
export class TextRenderer {
|
||||||
|
shinyChords = true;
|
||||||
|
|
||||||
|
shiny: number[] | undefined;
|
||||||
|
|
||||||
|
readonly cursorNode: SVGRectElement;
|
||||||
|
|
||||||
|
private readonly nodes = new Map<TextToken, SVGTextElement>();
|
||||||
|
|
||||||
|
private readonly heldNodes = new Map<string, SVGTextElement>();
|
||||||
|
|
||||||
|
private readonly occupiedHeld: Array<boolean | undefined> = [];
|
||||||
|
|
||||||
|
private readonly occupied: number[] = [];
|
||||||
|
|
||||||
|
animationOptions: KeyframeAnimationOptions = {
|
||||||
|
duration: 100,
|
||||||
|
easing: "ease",
|
||||||
|
};
|
||||||
|
|
||||||
|
heldKeySize = 0.8;
|
||||||
|
|
||||||
|
ghostText = "";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly node: HTMLElement,
|
||||||
|
readonly svg: SVGSVGElement,
|
||||||
|
readonly textNode: Text,
|
||||||
|
) {
|
||||||
|
this.cursorNode = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"rect",
|
||||||
|
);
|
||||||
|
this.cursorNode.setAttribute("x", "0");
|
||||||
|
this.cursorNode.setAttribute("y", "0");
|
||||||
|
this.svg.appendChild(this.cursorNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
set showCursor(value: boolean) {
|
||||||
|
this.cursorNode.style.visibility = value ? "visible" : "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
getAtRange(i: number): [number, number] {
|
||||||
|
const range = document.createRange();
|
||||||
|
const rangeIndex = Math.max(0, Math.min(i, this.textNode.length - 1));
|
||||||
|
range.setStart(this.textNode, rangeIndex);
|
||||||
|
range.setEnd(
|
||||||
|
this.textNode,
|
||||||
|
this.textNode.length === 0 ? 0 : rangeIndex + 1,
|
||||||
|
);
|
||||||
|
const charBounds = range.getBoundingClientRect();
|
||||||
|
return [
|
||||||
|
i > this.textNode.length - 1
|
||||||
|
? charBounds.x + charBounds.width
|
||||||
|
: charBounds.x,
|
||||||
|
charBounds.y + charBounds.height / 2 + 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
set held(keys: Map<string, boolean>) {
|
||||||
|
const prev = new Set(this.heldNodes.keys());
|
||||||
|
const fontSize = getComputedStyle(this.node).fontSize;
|
||||||
|
|
||||||
|
for (const [code, isHuman] of keys) {
|
||||||
|
if (!isHuman) continue;
|
||||||
|
prev.delete(code);
|
||||||
|
let node = this.heldNodes.get(code);
|
||||||
|
if (!node) {
|
||||||
|
let i = this.occupiedHeld.findIndex((it) => it === undefined);
|
||||||
|
if (i === -1) {
|
||||||
|
i = this.occupiedHeld.length;
|
||||||
|
this.occupiedHeld.push(true);
|
||||||
|
} else {
|
||||||
|
this.occupiedHeld[i] = true;
|
||||||
|
}
|
||||||
|
node = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||||
|
node.textContent = KBD_ICONS.get(code) ?? null;
|
||||||
|
node.setAttribute("i", i.toString());
|
||||||
|
this.heldNodes.set(code, node);
|
||||||
|
node.style.transform = `${this.cursorNode.style.transform} translateY(calc(${fontSize} * ${
|
||||||
|
i + 1.5
|
||||||
|
}))`;
|
||||||
|
node.style.fontSize = `calc(${fontSize} * ${this.heldKeySize})`;
|
||||||
|
this.svg.appendChild(node);
|
||||||
|
node
|
||||||
|
.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
transform: `translateY(calc(-${fontSize} * ${this.heldNodes.size})) scale(0)`,
|
||||||
|
},
|
||||||
|
{ transform: "translateY(0px) scale(1)" },
|
||||||
|
],
|
||||||
|
{ duration: 200, composite: "add", easing: "ease-out" },
|
||||||
|
)
|
||||||
|
.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const code of prev) {
|
||||||
|
const node = this.heldNodes.get(code);
|
||||||
|
if (!node) continue;
|
||||||
|
this.heldNodes.delete(code);
|
||||||
|
|
||||||
|
this.occupiedHeld[Number(node.getAttribute("i"))] = undefined;
|
||||||
|
node
|
||||||
|
.animate(
|
||||||
|
[
|
||||||
|
{ transform: "translateX(0px)" },
|
||||||
|
{ transform: "translateX(-10px)" },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 500,
|
||||||
|
composite: "accumulate",
|
||||||
|
easing: "ease-in",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.play();
|
||||||
|
const animation = node.animate([{ opacity: 1 }, { opacity: 0 }], {
|
||||||
|
duration: 500,
|
||||||
|
easing: "ease-in",
|
||||||
|
});
|
||||||
|
animation.onfinish = () => {
|
||||||
|
node.remove();
|
||||||
|
};
|
||||||
|
animation.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get animated(): boolean {
|
||||||
|
return this.cursorNode.classList.contains("animated");
|
||||||
|
}
|
||||||
|
|
||||||
|
set animated(value: boolean) {
|
||||||
|
if (value) {
|
||||||
|
this.cursorNode.classList.add("animated");
|
||||||
|
} else {
|
||||||
|
this.cursorNode.classList.remove("animated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set cursor(cursor: number) {
|
||||||
|
const bounds = this.node.getBoundingClientRect();
|
||||||
|
const style = getComputedStyle(this.node);
|
||||||
|
|
||||||
|
const pos = this.getAtRange(cursor);
|
||||||
|
const x = pos[0] - bounds.x;
|
||||||
|
const y = pos[1] - bounds.y;
|
||||||
|
|
||||||
|
this.cursorNode.setAttribute("height", style.fontSize);
|
||||||
|
this.cursorNode.setAttribute("width", "1");
|
||||||
|
|
||||||
|
this.cursorNode.style.transform = `translate(${x}px, calc(${y}px - ${style.fontSize} / 2))`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set text(text: TextToken[]) {
|
||||||
|
const prev = new Set(this.nodes.keys());
|
||||||
|
|
||||||
|
const bounds = this.node.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.svg.setAttribute("width", bounds.width.toFixed(2));
|
||||||
|
this.svg.setAttribute("height", bounds.height.toFixed(2));
|
||||||
|
this.svg.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
`0 0 ${bounds.width.toFixed(2)} ${bounds.height.toFixed(2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
text.forEach((token, i) => {
|
||||||
|
prev.delete(token);
|
||||||
|
let node = this.nodes.get(token);
|
||||||
|
|
||||||
|
const pos = this.getAtRange(i);
|
||||||
|
const x = pos[0] - bounds.x;
|
||||||
|
const y = pos[1] - bounds.y;
|
||||||
|
const xStr = x.toFixed(2);
|
||||||
|
const yStr = y.toFixed(2);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
node = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||||
|
this.nodes.set(token, node);
|
||||||
|
this.svg.appendChild(node);
|
||||||
|
node.setAttribute("x", xStr);
|
||||||
|
node.setAttribute("y", yStr);
|
||||||
|
node.setAttribute("i", i.toString());
|
||||||
|
if (token.source === "ghost") {
|
||||||
|
node.setAttribute("opacity", "0.5");
|
||||||
|
}
|
||||||
|
this.occupied[i] ??= 0;
|
||||||
|
if (this.animated) {
|
||||||
|
if (this.occupied[i] > 0) {
|
||||||
|
node
|
||||||
|
.animate([{ opacity: 0 }, { opacity: 1 }], {
|
||||||
|
...this.animationOptions,
|
||||||
|
easing: "ease-out",
|
||||||
|
})
|
||||||
|
.play();
|
||||||
|
} else {
|
||||||
|
node
|
||||||
|
.animate(
|
||||||
|
[
|
||||||
|
{ opacity: 0, transform: "translateY(10px)" },
|
||||||
|
{ opacity: 1, transform: "translateY(0px)" },
|
||||||
|
],
|
||||||
|
{ ...this.animationOptions, easing: "ease-out" },
|
||||||
|
)
|
||||||
|
.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.occupied[i]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token.correct) {
|
||||||
|
node.setAttribute("incorrect", "");
|
||||||
|
} else {
|
||||||
|
node.removeAttribute("incorrect");
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevX = node.getAttribute("x");
|
||||||
|
if (prevX && prevX !== xStr) {
|
||||||
|
const prev = parseFloat(prevX);
|
||||||
|
node.setAttribute("x", xStr);
|
||||||
|
/*if (this.animated) {
|
||||||
|
node.animate(
|
||||||
|
[{ transform: `translateX(${prev - x}px)` }, { transform: `translateX(0px)` }],
|
||||||
|
this.animationOptions
|
||||||
|
);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
const prevY = node.getAttribute("y");
|
||||||
|
if (prevY && prevY !== yStr) {
|
||||||
|
const prev = parseFloat(prevY);
|
||||||
|
node.setAttribute("y", yStr);
|
||||||
|
/*if (this.animated) {
|
||||||
|
node.animate(
|
||||||
|
[{ transform: `translateY(${prev - y}px)` }, { transform: `translateY(0px)` }],
|
||||||
|
this.animationOptions
|
||||||
|
);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
if (node.textContent !== token.text) {
|
||||||
|
node.textContent = token.text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const token of prev) {
|
||||||
|
const node = this.nodes.get(token)!;
|
||||||
|
const i = parseInt(node.getAttribute("i")!);
|
||||||
|
this.nodes.delete(token);
|
||||||
|
if (this.animated) {
|
||||||
|
const animation = node.animate(
|
||||||
|
[{ opacity: 1 }, { opacity: 0 }],
|
||||||
|
this.animationOptions,
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.occupied[i] === 1) {
|
||||||
|
node
|
||||||
|
.animate(
|
||||||
|
[
|
||||||
|
{ transform: "translateY(0px)" },
|
||||||
|
{ transform: "translateY(10px)" },
|
||||||
|
],
|
||||||
|
this.animationOptions,
|
||||||
|
)
|
||||||
|
.play();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
animation.onfinish = () => {
|
||||||
|
node.remove();
|
||||||
|
this.occupied[i]!--;
|
||||||
|
};
|
||||||
|
animation.play();
|
||||||
|
} else {
|
||||||
|
node.remove();
|
||||||
|
this.occupied[i]!--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isShiny(char: TextToken, index: number) {
|
||||||
|
return (
|
||||||
|
this.shiny?.includes(index) ||
|
||||||
|
(this.shinyChords && char.source === "robot")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/lib/chat/MatrixRoomMembers.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { RoomMember } from "matrix-js-sdk";
|
||||||
|
import { matrixClient, memberColor } from "./chat";
|
||||||
|
import { theme } from "$lib/preferences";
|
||||||
|
import { hexFromArgb } from "@material/material-color-utilities";
|
||||||
|
|
||||||
|
let { members }: { members: RoomMember[] } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="member-list">
|
||||||
|
{#each members as member (member.userId)}
|
||||||
|
{@const avatar = member.getMxcAvatarUrl()}
|
||||||
|
<div class="member">
|
||||||
|
{#if avatar}
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
src={$matrixClient.mxcUrlToHttp(avatar, 32, 32)}
|
||||||
|
alt={member.name}
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
{@const color = memberColor(member, $theme)}
|
||||||
|
{@const modeColor = $theme.mode === "dark" ? color.dark : color.light}
|
||||||
|
<div
|
||||||
|
style:background={hexFromArgb(modeColor.color)}
|
||||||
|
style:color={hexFromArgb(modeColor.onColor)}
|
||||||
|
class="avatar avatar-placeholder icon"
|
||||||
|
>
|
||||||
|
person
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<span>{member.name}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
251
src/lib/chat/MatrixTimeline.svelte
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {
|
||||||
|
EventTimeline,
|
||||||
|
MatrixEvent,
|
||||||
|
MsgType,
|
||||||
|
Room,
|
||||||
|
RoomEvent,
|
||||||
|
RoomMember,
|
||||||
|
RoomMemberEvent,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
import { onDestroy, onMount, tick } from "svelte";
|
||||||
|
import { matrixClient } from "./chat";
|
||||||
|
import MatrixEventComponent from "./events/MatrixEvent.svelte";
|
||||||
|
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
||||||
|
import { ReplayRecorder } from "$lib/charrecorder/core/recorder";
|
||||||
|
import { type Socket, io } from "socket.io-client";
|
||||||
|
import { SvelteMap } from "svelte/reactivity";
|
||||||
|
|
||||||
|
let { timeline }: { timeline: EventTimeline } = $props();
|
||||||
|
|
||||||
|
const excludeEvents = ["m.reaction", "m.room.redaction"];
|
||||||
|
|
||||||
|
let events = $state(
|
||||||
|
timeline
|
||||||
|
.getEvents()
|
||||||
|
.filter((it) => !excludeEvents.includes(it.getType()))
|
||||||
|
.reverse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let recorder = $state(new ReplayRecorder());
|
||||||
|
let showCursor = $state(false);
|
||||||
|
|
||||||
|
let timelineElement: HTMLElement = $state()!;
|
||||||
|
|
||||||
|
async function onTimeline(
|
||||||
|
event: MatrixEvent,
|
||||||
|
room?: Room,
|
||||||
|
toStartOfTimeline?: boolean,
|
||||||
|
) {
|
||||||
|
if (room?.roomId !== timeline.getRoomId()) return;
|
||||||
|
const sender = event.getSender();
|
||||||
|
if (sender) {
|
||||||
|
live.delete(sender);
|
||||||
|
}
|
||||||
|
if (excludeEvents.includes(event.getType())) return;
|
||||||
|
if (toStartOfTimeline) {
|
||||||
|
events.push(event);
|
||||||
|
} else {
|
||||||
|
const needScroll = timelineElement.scrollTop < 20;
|
||||||
|
events.unshift(event);
|
||||||
|
if (needScroll) {
|
||||||
|
await tick();
|
||||||
|
timelineElement.scroll({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let typing = $state<string[]>([]);
|
||||||
|
|
||||||
|
function onTyping(event: MatrixEvent, member: RoomMember) {
|
||||||
|
typing = event.event.content?.["user_ids"] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
const roomId = timeline.getRoomId();
|
||||||
|
if (!roomId) return;
|
||||||
|
const finalText = recorder.player.stepper.text
|
||||||
|
.map((token) => token.text)
|
||||||
|
.join("");
|
||||||
|
const finalRecording = recorder.finish();
|
||||||
|
if (!finalText) return;
|
||||||
|
recorder = new ReplayRecorder();
|
||||||
|
await $matrixClient.sendMessage(roomId, {
|
||||||
|
msgtype: "m.text" as MsgType.Text,
|
||||||
|
body: finalText,
|
||||||
|
// @ts-expect-error
|
||||||
|
"m.replay": finalRecording,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKey(event: KeyboardEvent) {
|
||||||
|
if (event.type === "keyup" && event.key === "Enter" && !event.shiftKey) {
|
||||||
|
send();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
recorder.next(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "keyup" && recorder.player.stepper.text.length === 0) {
|
||||||
|
recorder = new ReplayRecorder();
|
||||||
|
} else {
|
||||||
|
socket.emit("message", {
|
||||||
|
timeStamp: event.timeStamp,
|
||||||
|
type: event.type,
|
||||||
|
key: event.key,
|
||||||
|
code: event.code,
|
||||||
|
username: $matrixClient.getUserId(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let socket: Socket = $state()!;
|
||||||
|
let live = new SvelteMap<string, ReplayRecorder>();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
socket = io("https://srv.charachorder.io");
|
||||||
|
socket.emit("join", timeline.getRoomId());
|
||||||
|
|
||||||
|
socket.on("message", async ({ message }) => {
|
||||||
|
let userRecorder = live.get(message.username);
|
||||||
|
if (!userRecorder) {
|
||||||
|
userRecorder = new ReplayRecorder();
|
||||||
|
live.set(message.username, userRecorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
userRecorder.next(message);
|
||||||
|
|
||||||
|
if (userRecorder.player.stepper.text.length === 0) {
|
||||||
|
live.delete(message.username);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$matrixClient.on("Room.timeline" as RoomEvent.Timeline, onTimeline);
|
||||||
|
$matrixClient.on("RoomMember.typing" as RoomMemberEvent.Typing, onTyping);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
socket?.disconnect();
|
||||||
|
$matrixClient.off("Room.timeline" as RoomEvent.Timeline, onTimeline);
|
||||||
|
$matrixClient.off("RoomMember.typing" as RoomMemberEvent.Typing, onTyping);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div bind:this={timelineElement} class="timeline">
|
||||||
|
{#each live.entries() as [userId, recorder] (userId)}
|
||||||
|
{@const roomId = timeline.getRoomId()}
|
||||||
|
{#if roomId}
|
||||||
|
{@const room = $matrixClient.getRoom(roomId)}
|
||||||
|
{@const member = room?.getMember(userId)}
|
||||||
|
{#if member}
|
||||||
|
<MatrixEventComponent sender={member} replay={recorder.player} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{#each events as event, i (event.event["event_id"])}
|
||||||
|
{@const prev = events[i + 1]}
|
||||||
|
<MatrixEventComponent {event} sender={event.sender} {prev} {timeline} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="static-elements">
|
||||||
|
<div class="indicators"></div>
|
||||||
|
<div class="input-box">
|
||||||
|
<button class="icon">add</button>
|
||||||
|
<div
|
||||||
|
role="textbox"
|
||||||
|
tabindex="0"
|
||||||
|
class="input"
|
||||||
|
onkeydown={onKey}
|
||||||
|
onkeyup={onKey}
|
||||||
|
onfocusin={() => (showCursor = true)}
|
||||||
|
onfocusout={() => (showCursor = false)}
|
||||||
|
>
|
||||||
|
<CharRecorder replay={recorder.player} cursor={showCursor} />
|
||||||
|
</div>
|
||||||
|
<button class="icon" onclick={send}>send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$border-radius: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
height: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
|
flex-grow: 1;
|
||||||
|
cursor: text;
|
||||||
|
padding: 0.5em;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
|
text-wrap: wrap;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding-block: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.static-elements {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
contain: content;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-present {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-controls {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
min-height: 16px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
var(--md-sys-color-background)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
src/lib/chat/chat.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { derived, writable, type Writable } from "svelte/store";
|
||||||
|
import type {
|
||||||
|
ClientEvent,
|
||||||
|
LoginResponse,
|
||||||
|
MatrixClient,
|
||||||
|
RoomMember,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
import { persistentWritable } from "$lib/storage";
|
||||||
|
import {
|
||||||
|
themeFromSourceColor,
|
||||||
|
argbFromHex,
|
||||||
|
type CustomColorGroup,
|
||||||
|
} from "@material/material-color-utilities";
|
||||||
|
import type { UserTheme } from "$lib/preferences";
|
||||||
|
import { MatrixRx } from "./matrix-rx/client";
|
||||||
|
|
||||||
|
export const matrixClient: Writable<MatrixClient> = writable();
|
||||||
|
|
||||||
|
export const isLoggedIn: Writable<boolean> = writable(false);
|
||||||
|
|
||||||
|
export const matrix = derived(
|
||||||
|
[matrixClient, isLoggedIn],
|
||||||
|
([matrixClient, isLoggedIn]) =>
|
||||||
|
isLoggedIn ? new MatrixRx(matrixClient) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const currentRoomId = persistentWritable<string | null>(
|
||||||
|
"currentRoomId",
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
function getStoredLogin(): LoginResponse | undefined {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem("matrix-login")!);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeLogin(response: LoginResponse) {
|
||||||
|
localStorage.setItem("matrix-login", JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initMatrixClient() {
|
||||||
|
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
|
||||||
|
"matrix-js-sdk"
|
||||||
|
);
|
||||||
|
|
||||||
|
const storedLogin = getStoredLogin();
|
||||||
|
|
||||||
|
const store = new IndexedDBStore({
|
||||||
|
dbName: "matrix",
|
||||||
|
indexedDB: window.indexedDB,
|
||||||
|
});
|
||||||
|
const cryptoStore = new IndexedDBCryptoStore(
|
||||||
|
window.indexedDB,
|
||||||
|
"matrix-crypto",
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
baseUrl: import.meta.env.VITE_MATRIX_URL,
|
||||||
|
userId: storedLogin?.user_id,
|
||||||
|
accessToken: storedLogin?.access_token,
|
||||||
|
timelineSupport: true,
|
||||||
|
store,
|
||||||
|
cryptoStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("store");
|
||||||
|
await store.startup();
|
||||||
|
console.log("cryptoStore");
|
||||||
|
await cryptoStore.startup();
|
||||||
|
console.log("client");
|
||||||
|
await client.startClient();
|
||||||
|
client.once("sync" as ClientEvent.Sync, () => {
|
||||||
|
isLoggedIn.set(client.isLoggedIn());
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginToken = new URLSearchParams(window.location.search).get(
|
||||||
|
"loginToken",
|
||||||
|
);
|
||||||
|
if (loginToken) {
|
||||||
|
storeLogin(await client.loginWithToken(loginToken));
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
isLoggedIn.set(client.isLoggedIn());
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixClient.set(client);
|
||||||
|
console.log("done");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function memberColor(
|
||||||
|
member: RoomMember,
|
||||||
|
theme: UserTheme,
|
||||||
|
): CustomColorGroup {
|
||||||
|
let hash = 0;
|
||||||
|
member.userId.split("").forEach((char) => {
|
||||||
|
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
||||||
|
});
|
||||||
|
let color = "#";
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
|
color += value.toString(16).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeFromSourceColor(argbFromHex(theme.color), [
|
||||||
|
{ value: argbFromHex(color), name: "member", blend: true },
|
||||||
|
]).customColors.find((c) => c.color.name === "member")!;
|
||||||
|
}
|
||||||
357
src/lib/chat/events/MatrixEvent.svelte
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {
|
||||||
|
EventTimeline,
|
||||||
|
MatrixEvent,
|
||||||
|
MatrixEventEvent,
|
||||||
|
Relations,
|
||||||
|
RelationsEvent,
|
||||||
|
RoomMember,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
import MatrixMessageEvent from "./MatrixMessageEvent.svelte";
|
||||||
|
import { matrixClient, memberColor } from "../chat";
|
||||||
|
import { theme } from "$lib/preferences";
|
||||||
|
import { hexFromArgb } from "@material/material-color-utilities";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
import type { Replay } from "$lib/charrecorder/core/types";
|
||||||
|
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
||||||
|
import type { ReplayPlayer } from "$lib/charrecorder/core/player";
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
let {
|
||||||
|
event,
|
||||||
|
prev,
|
||||||
|
sender,
|
||||||
|
replay: replayPlayer,
|
||||||
|
timeline,
|
||||||
|
}: {
|
||||||
|
event?: MatrixEvent;
|
||||||
|
prev?: MatrixEvent;
|
||||||
|
sender?: RoomMember | null;
|
||||||
|
replay?: Replay | ReplayPlayer;
|
||||||
|
timeline?: EventTimeline;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let toolbarHover = $state(false);
|
||||||
|
let mainHover = $state(false);
|
||||||
|
|
||||||
|
let hover = $derived(toolbarHover || mainHover);
|
||||||
|
|
||||||
|
let replay: Replay | undefined = $state();
|
||||||
|
|
||||||
|
let reactions: Relations | undefined = $state(
|
||||||
|
timeline && event?.event.event_id
|
||||||
|
? timeline
|
||||||
|
.getTimelineSet()
|
||||||
|
.relations.getChildEventsForEvent(
|
||||||
|
event.event.event_id,
|
||||||
|
"m.annotation",
|
||||||
|
"m.reaction",
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
let annotations = writable<[string, Set<MatrixEvent>][] | null | undefined>();
|
||||||
|
|
||||||
|
function createRelations() {
|
||||||
|
if (!timeline || !event?.event.event_id) return;
|
||||||
|
reactions?.off("Relations.add" as RelationsEvent.Add, createRelations);
|
||||||
|
reactions?.off(
|
||||||
|
"Relations.remove" as RelationsEvent.Remove,
|
||||||
|
createRelations,
|
||||||
|
);
|
||||||
|
reactions = timeline
|
||||||
|
.getTimelineSet()
|
||||||
|
.relations.getChildEventsForEvent(
|
||||||
|
event.event.event_id,
|
||||||
|
"m.annotation",
|
||||||
|
"m.reaction",
|
||||||
|
);
|
||||||
|
reactions?.on("Relations.add" as RelationsEvent.Add, createRelations);
|
||||||
|
reactions?.on("Relations.remove" as RelationsEvent.Remove, createRelations);
|
||||||
|
reactions?.on(
|
||||||
|
"Relations.redaction" as RelationsEvent.Redaction,
|
||||||
|
createRelations,
|
||||||
|
);
|
||||||
|
annotations.set(
|
||||||
|
reactions?.getSortedAnnotationsByKey()?.filter(([, it]) => it.size > 0),
|
||||||
|
);
|
||||||
|
console.log("create");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
createRelations();
|
||||||
|
event?.on(
|
||||||
|
"Event.relationsCreated" as MatrixEventEvent.RelationsCreated,
|
||||||
|
createRelations,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
event?.off(
|
||||||
|
"Event.relationsCreated" as MatrixEventEvent.RelationsCreated,
|
||||||
|
createRelations,
|
||||||
|
);
|
||||||
|
reactions?.off("Relations.add" as RelationsEvent.Remove, createRelations);
|
||||||
|
reactions?.off(
|
||||||
|
"Relations.remove" as RelationsEvent.Remove,
|
||||||
|
createRelations,
|
||||||
|
);
|
||||||
|
reactions?.off(
|
||||||
|
"Relations.redaction" as RelationsEvent.Redaction,
|
||||||
|
createRelations,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="event"
|
||||||
|
role="log"
|
||||||
|
onmouseover={() => (mainHover = true)}
|
||||||
|
onfocus={() => (mainHover = true)}
|
||||||
|
onmouseout={() => (mainHover = false)}
|
||||||
|
onblur={() => (mainHover = false)}
|
||||||
|
>
|
||||||
|
{#if event && hover}
|
||||||
|
<div class="backdrop" transition:fade={{ duration: 100 }}></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if sender && !(prev && prev?.getType() === event?.getType() && prev.sender?.userId === event.sender?.userId)}
|
||||||
|
{@const color = memberColor(sender, $theme)}
|
||||||
|
{@const avatarMxc = sender.getMxcAvatarUrl()}
|
||||||
|
{#if avatarMxc}
|
||||||
|
{@const avatar = $matrixClient.mxcUrlToHttp(avatarMxc, 32, 32)}
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
src={avatar}
|
||||||
|
alt={sender.name}
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="avatar avatar-placeholder icon"
|
||||||
|
style:background={hexFromArgb(
|
||||||
|
$theme.mode === "dark" ? color.dark.color : color.light.color,
|
||||||
|
)}
|
||||||
|
style:color={hexFromArgb(
|
||||||
|
$theme.mode === "dark" ? color.dark.onColor : color.light.onColor,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
person
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="sender"
|
||||||
|
style:color={hexFromArgb(
|
||||||
|
$theme.mode === "dark" ? color.dark.color : color.light.color,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<strong>{sender.name}</strong>
|
||||||
|
{#if replay || replayPlayer}
|
||||||
|
<div class="dots">
|
||||||
|
{#each new Array(3) as _, i}
|
||||||
|
<div
|
||||||
|
style:animation-delay={i * 0.2 + "s"}
|
||||||
|
style:background={hexFromArgb(
|
||||||
|
$theme.mode === "dark" ? color.dark.color : color.light.color,
|
||||||
|
)}
|
||||||
|
class="dot"
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{#if event}
|
||||||
|
{#if event.getType() === "m.room.message"}
|
||||||
|
<MatrixMessageEvent {event} bind:replay />
|
||||||
|
{:else}
|
||||||
|
<details>
|
||||||
|
<summary>{event.getType()}</summary>
|
||||||
|
<pre>{JSON.stringify(event.event, null, 2)}</pre>
|
||||||
|
</details>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if replayPlayer}
|
||||||
|
<CharRecorder replay={replayPlayer} cursor={true} keys={true} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if event && hover}
|
||||||
|
<div
|
||||||
|
role="toolbar"
|
||||||
|
tabindex="0"
|
||||||
|
class="toolbar"
|
||||||
|
transition:fade={{ duration: 100 }}
|
||||||
|
onmouseover={() => (toolbarHover = true)}
|
||||||
|
onfocus={() => (toolbarHover = true)}
|
||||||
|
onmouseout={() => (toolbarHover = false)}
|
||||||
|
onblur={() => (toolbarHover = false)}
|
||||||
|
>
|
||||||
|
<button class="icon">add_reaction</button>
|
||||||
|
<button class="icon">reply</button>
|
||||||
|
{#if event.event.content?.["m.replay"]}
|
||||||
|
{#if replay}
|
||||||
|
<button class="icon" onclick={() => (replay = undefined)}>stop</button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="icon"
|
||||||
|
onclick={() => (replay = event.event.content?.["m.replay"])}
|
||||||
|
>replay</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<button class="icon">more_horiz</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $annotations && $annotations.length > 0}
|
||||||
|
<div class="reactions">
|
||||||
|
{#each $annotations as [reaction, events]}
|
||||||
|
<button class="reaction"
|
||||||
|
>{reaction} <span class="count">{events.size}</span></button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
details {
|
||||||
|
opacity: 0.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
text-wrap: wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: -26px;
|
||||||
|
right: 0;
|
||||||
|
background: var(--md-sys-color-secondary-container);
|
||||||
|
color: var(--md-sys-color-on-secondary-container);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bounce 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender,
|
||||||
|
.avatar {
|
||||||
|
margin-block: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
grid-area: avatar;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
translate: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.avatar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender {
|
||||||
|
display: flex;
|
||||||
|
grid-area: sender;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions {
|
||||||
|
grid-area: reactions;
|
||||||
|
margin-top: 2px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction {
|
||||||
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
> .count {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
display: grid;
|
||||||
|
position: relative;
|
||||||
|
padding-inline: 0.5em;
|
||||||
|
margin-inline: 0.5em;
|
||||||
|
padding-block: 0.25em;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
grid-template-areas:
|
||||||
|
"avatar sender date"
|
||||||
|
"avatar content content"
|
||||||
|
"none reactions reactions";
|
||||||
|
grid-template-columns: 32px 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
grid-area: content;
|
||||||
|
text-wrap: wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions,
|
||||||
|
.content,
|
||||||
|
.sender {
|
||||||
|
margin-inline: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.25;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
src/lib/chat/events/MatrixMessageEvent.svelte
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
||||||
|
import type { Replay } from "$lib/charrecorder/core/types";
|
||||||
|
import type { MatrixClient, MatrixEvent } from "matrix-js-sdk";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
import { matrixClient } from "../chat";
|
||||||
|
|
||||||
|
let { event, replay = $bindable() }: { event: MatrixEvent; replay?: Replay } =
|
||||||
|
$props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if event.event.content?.msgtype === "m.image"}
|
||||||
|
<img
|
||||||
|
src={$matrixClient.mxcUrlToHttp(event.event.content["url"])}
|
||||||
|
alt={event.event.content["body"]}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<span class="content" style:opacity={replay && 0}
|
||||||
|
>{event.event.content?.["body"]}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if replay}
|
||||||
|
<div class="replay" out:fade>
|
||||||
|
<CharRecorder
|
||||||
|
{replay}
|
||||||
|
cursor={true}
|
||||||
|
keys={true}
|
||||||
|
ondone={() => (replay = undefined)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
min-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 16em;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
src/lib/chat/matrix-rx/client.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Direction, MatrixClient, Room } from "matrix-js-sdk";
|
||||||
|
import {
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
type Observable,
|
||||||
|
of,
|
||||||
|
distinctUntilChanged,
|
||||||
|
merge,
|
||||||
|
} from "rxjs";
|
||||||
|
import { fromMatrixClientEvent } from "./events";
|
||||||
|
|
||||||
|
function roomListDistinct(prev: Room[], curr: Room[]) {
|
||||||
|
if (prev.length !== curr.length) return false;
|
||||||
|
for (let i = 0; i < prev.length; i++) {
|
||||||
|
if (prev[i]!.roomId !== curr[i]!.roomId) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatrixRx {
|
||||||
|
topLevelRooms$: Observable<Room[]>;
|
||||||
|
|
||||||
|
topLevelSpaces$: Observable<Room[]>;
|
||||||
|
|
||||||
|
topLevelChats$: Observable<Room[]>;
|
||||||
|
|
||||||
|
constructor(private client: MatrixClient) {
|
||||||
|
this.topLevelRooms$ = merge(
|
||||||
|
of([]),
|
||||||
|
fromMatrixClientEvent(client, "Room"),
|
||||||
|
fromMatrixClientEvent(client, "deleteRoom"),
|
||||||
|
fromMatrixClientEvent(client, "Room.myMembership"),
|
||||||
|
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
|
||||||
|
filter(
|
||||||
|
([_room, prev, curr]) =>
|
||||||
|
prev.getStateEvents("m.space.parent").length !==
|
||||||
|
curr.getStateEvents("m.space.parent").length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).pipe(
|
||||||
|
map(() =>
|
||||||
|
this.client.getVisibleRooms().filter(
|
||||||
|
(room) =>
|
||||||
|
room.getMyMembership() !== "leave" &&
|
||||||
|
room
|
||||||
|
.getLiveTimeline()
|
||||||
|
.getState("f" as Direction.Forward)
|
||||||
|
?.getStateEvents("m.space.parent").length === 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
distinctUntilChanged(roomListDistinct),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.topLevelSpaces$ = this.topLevelRooms$.pipe(
|
||||||
|
map((rooms) => rooms.filter((room) => room.isSpaceRoom())),
|
||||||
|
distinctUntilChanged(roomListDistinct),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.topLevelChats$ = this.topLevelRooms$.pipe(
|
||||||
|
map((rooms) => rooms.filter((room) => !room.isSpaceRoom())),
|
||||||
|
distinctUntilChanged(roomListDistinct),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpaceRx {
|
||||||
|
constructor(
|
||||||
|
private client: MatrixClient,
|
||||||
|
private space: Room,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
11
src/lib/chat/matrix-rx/events.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { ClientEventHandlerMap, MatrixClient } from "matrix-js-sdk";
|
||||||
|
import { fromEvent, type Observable } from "rxjs";
|
||||||
|
|
||||||
|
export function fromMatrixClientEvent<T extends keyof ClientEventHandlerMap>(
|
||||||
|
client: MatrixClient,
|
||||||
|
eventName: `${T}`, // hack so we can use strings instead of enums
|
||||||
|
): Observable<Parameters<ClientEventHandlerMap[T]>> {
|
||||||
|
return fromEvent(client, eventName) as Observable<
|
||||||
|
Parameters<ClientEventHandlerMap[T]>
|
||||||
|
>;
|
||||||
|
}
|
||||||
85
src/lib/chat/matrix-rx/rooms.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import type {
|
||||||
|
MatrixClient,
|
||||||
|
MatrixEvent,
|
||||||
|
Room,
|
||||||
|
Direction,
|
||||||
|
RoomState,
|
||||||
|
RoomStateEventHandlerMap,
|
||||||
|
EventType,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
import { fromMatrixClientEvent } from "./events";
|
||||||
|
import {
|
||||||
|
map,
|
||||||
|
filter,
|
||||||
|
merge,
|
||||||
|
startWith,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
fromEvent,
|
||||||
|
concat,
|
||||||
|
defer,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
export function matrixRoom$(
|
||||||
|
client: MatrixClient,
|
||||||
|
roomId: string | undefined,
|
||||||
|
): Observable<Room | undefined> {
|
||||||
|
return merge([
|
||||||
|
fromMatrixClientEvent(client, "Room").pipe(
|
||||||
|
filter(([room]) => room.roomId === roomId),
|
||||||
|
),
|
||||||
|
fromMatrixClientEvent(client, "deleteRoom").pipe(
|
||||||
|
filter(([id]) => id === roomId),
|
||||||
|
),
|
||||||
|
]).pipe(
|
||||||
|
startWith([]),
|
||||||
|
map(() => client.getRoom(roomId) ?? undefined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roomTimeline$(
|
||||||
|
client: MatrixClient,
|
||||||
|
room: Room | undefined,
|
||||||
|
): Observable<MatrixEvent[] | undefined> {
|
||||||
|
if (!room) return of(undefined);
|
||||||
|
const eventTimeline = room.getLiveTimeline();
|
||||||
|
|
||||||
|
return fromMatrixClientEvent(client, "Room.timeline").pipe(
|
||||||
|
filter(
|
||||||
|
([, eventRoom]) =>
|
||||||
|
eventRoom !== undefined && eventRoom.roomId === room.roomId,
|
||||||
|
),
|
||||||
|
startWith([]),
|
||||||
|
map(() => eventTimeline.getEvents()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roomCurrentStateEvents$(
|
||||||
|
client: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
eventType: EventType | string,
|
||||||
|
): Observable<MatrixEvent[]> {
|
||||||
|
return concat(
|
||||||
|
defer(() =>
|
||||||
|
of(
|
||||||
|
room
|
||||||
|
.getLiveTimeline()
|
||||||
|
.getState("f" as Direction.Forward)
|
||||||
|
?.getStateEvents(eventType) ?? [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
|
||||||
|
filter(([room]) => room.roomId === room.roomId),
|
||||||
|
map(([_room, _prev, curr]) => curr.getStateEvents(eventType)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromRoomStateEvent<T extends keyof RoomStateEventHandlerMap>(
|
||||||
|
state: RoomState,
|
||||||
|
eventName: `${T}`,
|
||||||
|
): Observable<Parameters<RoomStateEventHandlerMap[T]>> {
|
||||||
|
return fromEvent(state, eventName) as Observable<
|
||||||
|
Parameters<RoomStateEventHandlerMap[T]>
|
||||||
|
>;
|
||||||
|
}
|
||||||
19
src/lib/chat/matrix-rx/timeline.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { EventTimeline, MatrixClient, MatrixEvent } from "matrix-js-sdk";
|
||||||
|
import { filter, map, of, startWith, type Observable } from "rxjs";
|
||||||
|
import { fromMatrixClientEvent } from "./events";
|
||||||
|
|
||||||
|
export function roomTimeline(
|
||||||
|
client: MatrixClient,
|
||||||
|
roomId: string | undefined,
|
||||||
|
): Observable<MatrixEvent[]> {
|
||||||
|
if (!roomId) return of([]);
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) return of([]);
|
||||||
|
const eventTimeline = room.getLiveTimeline();
|
||||||
|
|
||||||
|
return fromMatrixClientEvent(client, "Room.timeline").pipe(
|
||||||
|
filter(([, room]) => room?.roomId === roomId),
|
||||||
|
startWith([]),
|
||||||
|
map(() => eventTimeline.getEvents()),
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/lib/components/Action.svelte
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<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";
|
||||||
|
|
||||||
|
let {
|
||||||
|
action,
|
||||||
|
display,
|
||||||
|
}: { action: number | KeyInfo; display: "inline-keys" | "keys" } = $props();
|
||||||
|
|
||||||
|
let info = $derived(
|
||||||
|
typeof action === "number"
|
||||||
|
? (KEYMAP_CODES.get(action) ?? { code: action })
|
||||||
|
: action,
|
||||||
|
);
|
||||||
|
let dynamicMapping = $derived(info.keyCode && $osLayout.get(info.keyCode));
|
||||||
|
|
||||||
|
let tooltip = $derived(
|
||||||
|
`<${info.id ?? `0x${info.code.toString(16)}`}> ` +
|
||||||
|
(info.title ?? "") +
|
||||||
|
(info.variant === "left"
|
||||||
|
? " (left)"
|
||||||
|
: info.variant === "right"
|
||||||
|
? " (right)"
|
||||||
|
: ""),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if display === "keys"}
|
||||||
|
<kbd
|
||||||
|
class:icon={!!info.icon}
|
||||||
|
class:left={info.variant === "left"}
|
||||||
|
class:right={info.variant === "right"}
|
||||||
|
use:title={{ title: tooltip }}
|
||||||
|
>
|
||||||
|
{dynamicMapping ??
|
||||||
|
info.icon ??
|
||||||
|
info.display ??
|
||||||
|
info.id ??
|
||||||
|
`0x${info.code.toString(16)}`}
|
||||||
|
</kbd>
|
||||||
|
{:else if display === "inline-keys"}
|
||||||
|
{#if !info.icon && dynamicMapping?.length === 1}
|
||||||
|
<span
|
||||||
|
use:title={{ title: tooltip }}
|
||||||
|
class:left={info.variant === "left"}
|
||||||
|
class:right={info.variant === "right"}>{dynamicMapping}</span
|
||||||
|
>
|
||||||
|
{:else if !info.icon && info.id?.length === 1}
|
||||||
|
<span
|
||||||
|
use:title={{ title: tooltip }}
|
||||||
|
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 }}
|
||||||
|
>
|
||||||
|
{dynamicMapping ??
|
||||||
|
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>
|
||||||
105
src/lib/components/ActionListItem.svelte
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<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";
|
||||||
|
import type { MouseEventHandler } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
id,
|
||||||
|
onclick,
|
||||||
|
}: { id: number | KeyInfo; onclick?: MouseEventHandler<HTMLButtonElement> } =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
let key = $derived(
|
||||||
|
(typeof id === "number" ? KEYMAP_CODES.get(id) ?? id : id) as
|
||||||
|
| number
|
||||||
|
| KeyInfo,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button {onclick}>
|
||||||
|
{#if typeof key === "object"}
|
||||||
|
<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}
|
||||||
|
{#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>
|
||||||
|
<Action display="keys" action={key} />
|
||||||
|
{:else}
|
||||||
|
<span class="key">0x{key.toString(16)}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
font-family: "Noto Sans Mono", monospace;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
@media not (forced-colors: active) {
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
border: 1px solid ButtonBorder;
|
||||||
|
margin-block: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ActiveText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
|
|
||||||
|
> :global(.icon) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/lib/components/ActionString.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Action from "$lib/components/Action.svelte";
|
||||||
|
import type { KeyInfo } from "$lib/serial/keymap-codes";
|
||||||
|
|
||||||
|
let {
|
||||||
|
actions,
|
||||||
|
display = "inline-keys",
|
||||||
|
}: { actions: Array<number | KeyInfo>; display?: "keys" | "inline-keys" } =
|
||||||
|
$props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each actions as action, i (`${typeof action === "number" ? action : action.code}:${i}`)}
|
||||||
|
<Action {action} {display} />
|
||||||
|
{/each}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
<script>
|
|
||||||
import RingInput from "$lib/components/RingInput.svelte"
|
|
||||||
|
|
||||||
let activeLayer = 0
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
{#each [["Numeric Layer", "123", 1], ["Primary Layer", "abc", 0], ["Function Layer", "function", 2]] as [title, icon, value]}
|
|
||||||
<button {title} class="icon" on:click={() => (activeLayer = value)} class:active={activeLayer === value}>
|
|
||||||
{icon}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
fieldset {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
margin-block-end: -36px;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.icon {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
aspect-ratio: 1;
|
|
||||||
|
|
||||||
font-size: 32px;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
outline: 8px solid var(--md-sys-color-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-inline-end: 16px;
|
|
||||||
border-radius: 16px 0 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
padding-inline-start: 16px;
|
|
||||||
border-radius: 0 16px 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.row,
|
|
||||||
.col {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {serialPort, syncStatus} from "$lib/serial/connection"
|
|
||||||
import {browser} from "$app/environment"
|
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {slide} from "svelte/transition"
|
|
||||||
|
|
||||||
const training = [
|
|
||||||
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
|
||||||
{slug: "chords", title: "ChM - Chords Mastered", icon: "piano"},
|
|
||||||
{slug: "avg-wpm", title: "aWPM - Average Words Per Minute", icon: "avg_pace"},
|
|
||||||
{slug: "sentences", title: "StM - Sentences Mastered", icon: "lyrics"},
|
|
||||||
{slug: "top-wpm", title: "tWPM - Top Words Per Minute", icon: "speed"},
|
|
||||||
{slug: "cm", title: "CM - Concepts Mastered", icon: "cognition"},
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<a href="/" class="title">dot i/o</a>
|
|
||||||
|
|
||||||
<div class="steps">
|
|
||||||
{#each training as { slug, title, icon }}
|
|
||||||
<a
|
|
||||||
href="/train/{slug}/"
|
|
||||||
{title}
|
|
||||||
class="icon train {slug}"
|
|
||||||
class:active={$page.url.pathname === `/train/${slug}/`}>{icon}</a
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
|
||||||
<PwaStatus />
|
|
||||||
{/await}
|
|
||||||
{#if browser && !("serial" in navigator)}
|
|
||||||
<abbr
|
|
||||||
title="Your browser does not support serial connections. Try using Chrome instead."
|
|
||||||
class="icon error"
|
|
||||||
>
|
|
||||||
warning
|
|
||||||
</abbr>
|
|
||||||
{/if}
|
|
||||||
<a
|
|
||||||
title="Backup & Restore"
|
|
||||||
href="/backup/"
|
|
||||||
class="icon {$syncStatus}"
|
|
||||||
class:active={$page.url.pathname.startsWith("/backup/")}
|
|
||||||
>
|
|
||||||
{#if $syncStatus === "downloading"}
|
|
||||||
backup
|
|
||||||
{:else if $syncStatus === "uploading"}
|
|
||||||
cloud_download
|
|
||||||
{:else}
|
|
||||||
cloud_done
|
|
||||||
{/if}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/config/"
|
|
||||||
title="Device Manager"
|
|
||||||
class="icon connect"
|
|
||||||
class:active={$page.url.pathname.startsWith("/config/")}
|
|
||||||
class:error={$serialPort === undefined}
|
|
||||||
>
|
|
||||||
cable
|
|
||||||
</a>
|
|
||||||
<a href="/" title="Statistics" class="icon account">person</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@keyframes sync {
|
|
||||||
0% {
|
|
||||||
scale: 1 1;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
85% {
|
|
||||||
scale: 1 0;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
86% {
|
|
||||||
scale: 1 1;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
scale: 1 1;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploading::after,
|
|
||||||
.downloading::after {
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 50%;
|
|
||||||
transform-origin: top;
|
|
||||||
translate: -50% 0;
|
|
||||||
|
|
||||||
width: 8px;
|
|
||||||
height: 10px;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-background);
|
|
||||||
|
|
||||||
animation: sync 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloading.active::after,
|
|
||||||
.uploading.active::after {
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync.downloading::after {
|
|
||||||
top: 10px;
|
|
||||||
transform-origin: bottom;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
margin-block: 8px;
|
|
||||||
margin-inline: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-block: 0;
|
|
||||||
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
aspect-ratio: 1;
|
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
background: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> a.icon {
|
|
||||||
aspect-ratio: unset;
|
|
||||||
margin-inline: -4px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--md-sys-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
clip-path: polygon(25% 50%, 0% 0%, 75% 0%, 100% 50%, 75% 100%, 0% 100%);
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
|
|
||||||
&,
|
|
||||||
~ * {
|
|
||||||
translate: 8px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon.account {
|
|
||||||
font-size: 32px;
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
|
||||||
background: var(--md-sys-color-secondary-container);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||