156 Commits

Author SHA1 Message Date
6faaa18b3b 1.3.2 2024-01-30 19:49:52 +01:00
6ab6959129 fix: disallow null inputs when editing
feat: allow special inputs while creating a chord input
fixes #93
2024-01-30 19:49:10 +01:00
44d89d3f35 1.3.1 2024-01-24 18:55:46 +01:00
eaf0adaf01 fix: sort legacy chord inputs 2024-01-24 18:55:31 +01:00
5b6a5ea36d 1.3.0 2024-01-20 22:24:39 +01:00
14cbb5553b feat: add auto-space info 2024-01-20 22:24:00 +01:00
duianto
8ed72fe958 fix: typo 2024-01-11 09:36:33 +01:00
06b83f79ef feat: add refresh button
resolves #82
2024-01-05 00:12:42 +01:00
5fa4b1fd09 1.2.0 2024-01-03 14:59:12 +01:00
f585a0ebda fix: disable Tauri publish for now 2024-01-03 14:50:51 +01:00
a48e2b5a16 fix: keyinfo missing display type prop 2024-01-03 14:21:37 +01:00
fd612eda1d fix: dynamic mappings are not displayed 2024-01-03 14:21:13 +01:00
a1fe6f7110 feat: periodically update os-layout in the background
fix: remove dead code in layout detection
fixes #78
resolves #79
2024-01-03 13:55:50 +01:00
0e57e810e0 feat: change icons 2024-01-03 01:26:39 +01:00
a15d5dde38 feat: inform user when save action failed
fixes #67
2023-12-30 16:04:16 +01:00
560206129e feat: add meta for config pages 2023-12-30 16:03:31 +01:00
cb7c70dac1 refactor: flatten visual key positioning system
fixes #74
fixes #43
2023-12-30 15:50:48 +01:00
edabf8ec84 fix: settings without min/max parse as 0
fixes #75
2023-12-30 12:50:46 +01:00
f2f61f32f2 feat: add reset options
resolves #70
2023-12-29 15:04:33 +01:00
a3857843d6 feat: use keycodes on CCX
resolves #71
2023-12-29 13:48:34 +01:00
c1b1068c4b fix: settings.yml missing hex prefix
feat: add direnv config
2023-12-29 13:23:41 +01:00
2411dd2bea feat: show dynamic key maps in layout view 2023-12-22 12:51:23 +01:00
7911904906 Revert "refactor: remove outdated files"
This reverts commit 84b22e0006.
2023-12-21 23:55:29 +01:00
630687de80 fix: use hexadecimal for settings 2023-12-21 23:49:55 +01:00
84b22e0006 refactor: remove outdated files 2023-12-21 23:39:23 +01:00
dd070c8856 feat: include source maps in pwa 2023-12-21 21:31:43 +01:00
6872cd0554 fix: build 2023-12-21 21:24:12 +01:00
628007af23 fix: align settings wording with gtm 2023-12-21 21:23:00 +01:00
19fad84357 fix: svg invalid 2023-12-21 21:20:26 +01:00
f172318a78 feat: update logo and favicon 2023-12-21 21:19:17 +01:00
c2e3850082 fix: maybe fix cloudflare pages loop 2023-12-21 20:47:06 +01:00
7a5a4eb434 feat: update pwa icon 2023-12-21 20:46:12 +01:00
c878311f62 feat: add web manifest to site meta 2023-12-21 20:28:47 +01:00
fb3fb246e9 fix: remove json files from pwa glob pattern 2023-12-21 20:19:55 +01:00
b4e4ca84a4 fix: pwa tries to include build-only files 2023-12-21 20:05:32 +01:00
c1b1544256 fix: mouse & scroll-speed options are unstyled
resolves #44
2023-12-21 19:42:37 +01:00
03dd528465 feat: add ability to add special actions to chord inputs
resolves #10
2023-12-21 19:19:47 +01:00
81af9f2e82 feat: add min/max enforcement to device settings
resolves #6
2023-12-21 18:31:50 +01:00
6bb42429e5 feat: add change indicator on settings page 2023-12-21 18:23:47 +01:00
d07751a944 fix: remove OS setting
resolves #61
2023-12-21 18:12:37 +01:00
8867030ede fix: PWA fixes 2023-12-21 18:08:16 +01:00
faaa6dd5be fix: keyboard action variant wrong 2023-12-21 16:11:02 +01:00
43cf13094e fix: "p" key missing for ccx and linked with "o" key 2023-12-20 20:01:28 +01:00
ed523628ff feat: try typing field in chords section
resolves #68
fix: "No Results" not translated
2023-12-18 18:42:08 +01:00
98b451eec9 1.1.0 2023-12-17 00:33:11 +01:00
6e37dc198f feat: rework character timeout setting 2023-12-16 18:13:02 +01:00
e319b1bfaf fix: swap top/bottom thumb labels
fixes #65
2023-12-16 15:27:34 +01:00
eb33b64100 feat: reject new chords that override another chord 2023-12-16 15:24:51 +01:00
766bc44a85 feat: do not use empty phrase for deleted chords 2023-12-16 15:20:44 +01:00
b679aa377a fix: key text showing focus outline
fix: layout selectable
2023-12-16 13:11:52 +01:00
ea3192d4e6 feat: add links to docs and dotio 2023-12-16 12:46:20 +01:00
256daec412 feat: chord modifier hints 2023-12-15 19:54:31 +01:00
29a07133d1 fix: deadlock 2023-12-15 16:59:06 +01:00
c3bd8431e5 feat: debounce connection suspension 2023-12-15 16:43:56 +01:00
c8e04ed6cc feat: auto-reconnect after reboot 2023-12-12 18:37:40 +01:00
d98653995b feat: bootloader warning
refactor: reword linux premission warning
2023-12-12 18:30:20 +01:00
3dd9611ebf feat: Linux permission guide 2023-12-12 18:03:34 +01:00
9d9360375b 1.0.0 2023-12-08 23:52:33 +01:00
d683c8c70c fix: action selector shows next item every time 2023-12-08 23:21:21 +01:00
d8d430f333 fix: browser warning referencing a non-existent app 2023-12-08 23:12:52 +01:00
fe850f47ec feat: add info for current and after next save action 2023-12-08 23:04:38 +01:00
f9a63a8724 fix: warn users if no device is connected
fix: can't backup without a device
2023-12-08 22:55:33 +01:00
af01426f43 fix: action tooltips not updating 2023-12-08 22:46:01 +01:00
9d7cefb3b4 fix: add ui when no device is connected
fixes #60
2023-12-08 22:38:46 +01:00
f44e5a79de fix: action selector search bar color
fixes #39
2023-12-08 22:30:03 +01:00
8b2e92c124 feat: add icons to unassigned cc1 3D keys
fixes #58
2023-12-08 22:26:32 +01:00
f758be91a9 fix: build 2023-12-08 22:16:07 +01:00
bf4c86e698 fix: legacy chords with commas and spaces 2023-12-08 22:10:49 +01:00
50a09d2008 fix: PWA not working 2023-12-08 22:02:48 +01:00
3c1a4de4a7 fix: chord page overlapping
fixes #57, fixes #56
fix: handle trailing spaces in lecacy chord files
2023-12-08 21:59:08 +01:00
8cbdf1393f fix: chord files not detected properly
feat: alert on unknown backups
2023-12-08 21:14:37 +01:00
1ccb17f053 fix: allow trailing linebreak for legacy layouts 2023-12-08 21:04:18 +01:00
532dc70fe2 feat: ccx layout 2023-12-08 14:49:07 +01:00
d5893013f9 feat: ccx layout maybe 2023-12-08 14:13:31 +01:00
80308cad73 feat: ccx layout (hopefully) 2023-12-07 21:52:04 +01:00
2d59bd016f feat: ccx row 2 2023-12-07 21:40:27 +01:00
298de49257 test ccx row 2023-12-07 21:36:31 +01:00
3a62864a41 feat: ccx key count 2023-12-07 21:29:26 +01:00
109095e35e feat: re-introduce background sync 2023-12-07 19:51:23 +01:00
2dd6f39ac6 fix: reword alt-code warnings
fixes #11
2023-12-07 19:36:10 +01:00
b0f653e73b fix: weird input behaviour on setting changes 2023-12-07 19:27:29 +01:00
d552fb9220 fix: only import settings that already exist 2023-12-07 19:06:49 +01:00
77339620e6 fix: full backups fail because of invalid setting IDs 2023-12-07 18:58:40 +01:00
846183bbb1 feat: compound chording actions 2023-12-06 01:19:01 +01:00
1d53f6df7a fix: crash with missing action info in chords 2023-12-06 00:31:08 +01:00
58d13a4107 feat: enable source maps in production builds 2023-12-05 17:29:15 +01:00
f7d99d8d7b feat: dynamic keymap prototype 2023-12-03 00:01:51 +01:00
d9dd003b01 feat: show warnings about shift and alt-code macros
resolves #38
2023-12-02 23:26:04 +01:00
dc798d2b9f v0.7.0 2023-12-02 21:23:34 +01:00
c2ec460c8c feat: alert user when connection failed, resolves #53 2023-12-02 21:18:56 +01:00
c51bcc8ff0 feat: legacy backup import, resolves #31 2023-12-02 21:16:22 +01:00
63b7f8ab18 feat: inform user when backup is incompatible, resolves #34 2023-12-02 21:03:14 +01:00
eaf8028538 feat: chord sharing url, resolves #40 2023-12-02 20:59:58 +01:00
2ad0ef3b6d feat: show info about key in chord manager and layout, resolves #51, resolves #52 2023-12-02 20:42:33 +01:00
20705de069 fix: editing chords bouces back to page 1, fixes #22 2023-12-02 20:31:39 +01:00
64b519d5b1 fix: can't type in the terminal 2023-12-02 20:25:10 +01:00
fb490b3db6 fix: page transitions can be buggy, fixes #55, #23 2023-12-02 20:24:42 +01:00
c37ae7da7b refactor: adjust wording for backups 2023-12-02 20:23:15 +01:00
5c06c2206c fix: undo/redo prevents use of unknown actions 2023-12-02 19:31:46 +01:00
f9cdf70bdb change save button styling
resolves #49
2023-11-29 14:31:19 +01:00
3a6483aa61 feat: combine save/apply
resolves #45
2023-11-29 01:08:46 +01:00
Priyanshu Tripathi
018c7a5eac fix: link to the new repository
The old repo will soon fall behind in terms of releases and the link will become outdated.
2023-11-28 17:35:43 +01:00
f73b8c1453 fix: set auto-connect to false by default
fixes #25
2023-11-26 22:17:52 +01:00
e38d952e1d fix: imported chords not filtered 2023-11-18 18:59:30 +01:00
8e5692ca59 fix: imported chords not filtered 2023-11-18 18:49:15 +01:00
a0fe925ea9 feat: basic chord trainer
fix: don't add chords from backup if identical chords already exist, fixes #30
2023-11-18 18:35:59 +01:00
e84470d577 fix: chord actions not sorted 2023-11-18 15:53:07 +01:00
683561dc06 feat: chord backup import 2023-11-18 11:21:50 +01:00
2fd2dad6f7 fix: chord maps are ordered incorrectly with new chords, fixes #24 2023-11-18 01:53:49 +01:00
e2f9f87b13 fix: very large toggles 2023-11-15 02:19:10 +01:00
623d895aea fix: broken site 2023-11-15 02:05:44 +01:00
561300de64 refactor: cleanup 2023-11-15 01:46:23 +01:00
c5d9defc9d feat: layout url import
feat: backup import (except chords)
feat: legacy layout import
feat: separate layout, chord & setting backup downloads
2023-11-15 01:14:34 +01:00
acd58646f6 feat: add generid 103-key layout for CCX users, fixes #12 2023-11-14 23:37:06 +01:00
3634264af3 fix: chentry disables unrelated settings, fixes #8 2023-11-14 23:03:34 +01:00
3515994a5a fix: settings page doesn't let you input more than one number, fixes #5 2023-11-14 23:00:18 +01:00
bdebe238ae feat: auto-show connect dialog when auto-connect is disabled, resolves #14 2023-11-14 22:51:59 +01:00
ebf7d73d20 feat: new blocking progress bar, fixes #18
feat: change cloud icon to history, fixes #15
fix: action search items overlap, fixes #16
feat: show tooltips immediately
2023-11-14 20:19:01 +01:00
e19a57efac feat: new chord button, fixes #9
feat: improved backups
2023-11-10 17:31:52 +01:00
034436f93e fix: editing chords messes up list 2023-11-10 16:05:42 +01:00
2710f7fc25 fix: phrase insert button not working 2023-11-10 16:00:34 +01:00
d2276a53d0 feat: chord editing 2023-11-10 15:45:04 +01:00
8701d7a40d fix: strikethrough not showing 2023-11-10 01:30:55 +01:00
94cfaf40e5 feat: new chord editing
feat: clear all changes with shift undo, fixes #7
2023-11-10 01:17:36 +01:00
c661a4b30b fix: chords can't be deleted 2023-11-03 23:13:56 +01:00
9b95e1d67a refactor: update branding
Fixes #4
2023-11-03 22:45:30 +01:00
f7bf93fcfc feat: chord editing prototype
feat: printing style for layout
2023-11-03 22:37:27 +01:00
08df049170 feat: rudimentary filter in action selector
Fixes #1
2023-11-03 18:57:22 +01:00
65a536cdea fix: chord deletion outputs empty string
Fixes #3
2023-11-03 18:26:58 +01:00
d2fd84a6b5 fix: add vendor ids for additional devices
fix: use proper semver parsing for device versions

Fixes #2
2023-11-03 18:24:32 +01:00
88429412b9 fix: lite breaks layout viewer 2023-11-02 22:13:37 +01:00
ef309d603e feat: editing 2023-11-02 00:16:18 +01:00
fade2f978e fix: cc1 layout keys 2023-11-01 17:03:38 +01:00
a1760d518c fix: layout keys 2023-11-01 16:40:18 +01:00
9d33565081 fix: search page pagination 2023-11-01 16:24:29 +01:00
Raymond Li
11fe12f095 Put CNAME into build.yml 2023-10-31 18:36:13 -04:00
aba390839b add cc1 visual layout 2023-10-31 23:27:44 +01:00
Raymond Li
a6e7df55ff Disable jekyll in build.yml 2023-10-31 18:12:16 -04:00
Raymond Li
7e5e7b8f5f Update build.yml 2023-10-31 17:50:43 -04:00
Raymond Li
a34ba35889 Remove extra checkout in build.yml 2023-10-31 17:49:54 -04:00
Raymond Li
616d15b6bd Merge jobs in build.yml 2023-10-31 17:48:59 -04:00
Raymond Li
283444f0be Fix build.yml 2023-10-31 17:44:31 -04:00
Raymond Li
e5e56c04a2 Update build.yml to deploy to GitHub pages 2023-10-31 17:34:10 -04:00
Raymond Li
a34c176bcc Update README.md to official CharaChorder implementation 2023-10-31 17:22:44 -04:00
e4d51cd51d visual layout adjustments 2023-10-31 22:09:33 +01:00
a7b49de6ac lite layout 2023-10-31 18:22:03 +01:00
fc86b31337 feat: chord editing prototype
feat: lazy device connections
feat: backup docs
feat: chord library pagination
2023-10-27 19:39:26 +02:00
d8f0679233 keyboard stuff, styling things 2023-09-25 18:12:34 +02:00
c93246ee8c 0.6.5 2023-09-22 20:51:42 +02:00
22905c2b56 0.6.4 2023-09-22 20:49:29 +02:00
074f1da48d update version hook 2023-09-22 20:49:29 +02:00
e7a52221d2 feat: layout editing (sorta) 2023-09-22 20:27:15 +02:00
f03b4d586b feat: version and issue url 2023-09-22 14:15:01 +02:00
4cd9ce536d feat: new sharing system
feat: support legacy layout import
2023-09-16 14:17:59 +02:00
a39f57bac1 feat: apply setting changes and add commit feature 2023-09-07 17:39:33 +02:00
bf96c1e29d feat: include dev tools in releases 2023-08-04 22:38:18 +02:00
138 changed files with 6556 additions and 2555 deletions

1
.envrc Normal file
View File

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

View File

@@ -2,13 +2,13 @@ name: Build
on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
tags:
- "v*"
workflow_dispatch:
jobs:
build:
name: 🔨 Build
CI:
name: 🔨🚀 Build and deploy
runs-on: ubuntu-latest
steps:
- name: 🚚 Checkout
@@ -39,26 +39,12 @@ jobs:
with:
name: build
path: build
deploy:
name: 🚀 Deploy
if: github.event_name == 'push' && contains(github.event.head_commit.message, '[deploy]')
runs-on: ubuntu-latest
needs: build
environment:
name: Website
url: https://dotio.theaninova.de
steps:
- name: 📦 Download build artifacts
uses: actions/download-artifact@v2.1.1
with:
name: build
path: build
- name: 🚀 Deploy
uses: SamKirkland/web-deploy@v1
with:
target-server: ${{ secrets.SSH_SERVER }}
destination-path: ~/public_html/
source-path: ./build/
remote-user: ${{ secrets.SSH_USER }}
private-ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-port: ${{ secrets.SSH_PORT }}
- name: Disable jekyll
run: touch build/.nojekyll
- name: Custom domain
run: echo 'manager.charachorder.com' > build/CNAME
- run: git config user.name github-actions
- run: git config user.email github-actions@github.com
- run: git --work-tree build add --all
- run: git commit -m "Automatic Deploy action run by github-actions"
- run: git push origin HEAD:gh-pages --force

View File

@@ -1,8 +1,8 @@
name: 'publish'
name: "publish desktop apps"
on:
push:
tags:
- 'v*'
- "desktop-app-v*"
workflow_dispatch:
jobs:
@@ -48,7 +48,7 @@ jobs:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
with:
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
releaseName: 'App v__VERSION__'
releaseBody: 'See the assets to download this version and install.'
releaseName: "App v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false

1
.gitignore vendored
View File

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

View File

@@ -6,6 +6,7 @@ node_modules
.env
.env.*
!.env.example
/src-tauri/target
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml

View File

@@ -1,9 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
}

6
.prettierrc.cjs Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
...require("@theaninova/prettier-config"),
plugins: ["prettier-plugin-svelte"],
pluginSearchDirs: ["."],
overrides: [{files: "*.svelte", options: {parser: "svelte"}}],
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/typesafe-i18n@5.26.0/schema/typesafe-i18n.json",
"baseLocale": "en",
"adapter": "svelte"
}
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
"baseLocale": "en",
"adapter": "svelte"
}

37
CONTRIBUTING.md Normal file
View 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.

View File

@@ -1,40 +1,12 @@
# amaCC1ng
# CharaChorder Device Manager
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/Theaninova/dotio/build.yml)
![GitHub](https://img.shields.io/github/license/Theaninova/dotio)
[![GitHub deployments](https://img.shields.io/github/deployments/Theaninova/dotio/Website?label=delployment)](https://dotio.theaninova.de/)
The official device manager and configuration tool for CharaChorder devices.
_This project is not affiliated or endorsed with neither the original [dot i/o](https://www.iq-eq.io/) site, nor [CharaChorder](https://www.charachorder.com/)_
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/CharaChorder/DeviceManager/build.yml)
![GitHub](https://img.shields.io/github/license/CharaChorder/DeviceManager)
[![GitHub deployments](https://img.shields.io/github/deployments/CharaChorder/DeviceManager/Website?label=delployment)](https://manager.charachorder.com/)
Get the latest desktop release [here](https://github.com/Theaninova/dotio/releases).
I aim to create a new site that offers an easier, visually pleasing
and more complete way to configure and learn CharaChorder devices.
## Development
### Nix
[Enable flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes), then start the development shell using
```shell
nix develop
```
You may need to run through some additional setup to get Rust running inside IntelliJ.
### Other platforms
- NodeJS >=18.16
- Python >=3.10
- Rust Stable (For Tauri Development)
I know, python in JS projects is extremely annoying, unfortunately,
it seems to be the only platform that offers a functional
way to subset variable woff2 fonts with ligatures.
In other words, either have python as a development dependency or
serve a 3.5MB icons font of which 99.5% is completely unused.
Get the latest desktop release [here](https://github.com/CharaChorder/DeviceManager/releases).
## Deployment
@@ -57,3 +29,11 @@ To double-check, make sure your private key starts with
After that, add the `SSH_SERVER`, `SSH_PORT`, `SSH_PRIVATE_KEY` and `SSH_USER`
environment secrets to your environment in GitHub.
## Releases
Change the version in
- [package.json](package.json)
- [tauri.conf.json](src-tauri/tauri.conf.json)
- [Cargo.toml](src-tauri/Cargo.toml)

64
docs/BACKUP.md Normal file
View 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.

View File

@@ -4,31 +4,37 @@
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
rust-bin = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-std" "clippy" "rust-analyzer" ];
};
fontMin = (pkgs.python311.withPackages(ps: with ps; [ brotli fonttools ] ++ (with fonttools.optional-dependencies; [ woff ])));
tauriPkgs = nixpkgs.legacyPackages.${system};
libraries = with tauriPkgs; [
webkitgtk
gtk3
cairo
gdk-pixbuf
glib
dbus
openssl_3
librsvg
];
packages = (with pkgs; [
outputs = {
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
flake-utils.lib.eachDefaultSystem (system: let
overlays = [(import rust-overlay)];
pkgs = import nixpkgs {inherit system overlays;};
rust-bin = pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src" "rust-std" "clippy" "rust-analyzer"];
};
fontMin = pkgs.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
tauriPkgs = nixpkgs.legacyPackages.${system};
libraries = with tauriPkgs; [
webkitgtk
gtk3
cairo
gdk-pixbuf
glib
dbus
openssl_3
librsvg
];
packages =
(with pkgs; [
nodejs_18
rust-bin
fontMin
]) ++ (with tauriPkgs; [
])
++ (with tauriPkgs; [
curl
wget
pkg-config
@@ -39,16 +45,15 @@
libsoup
webkitgtk
librsvg
# serial plugin
udev
# serial plugin
udev
]);
in
{
devShell = pkgs.mkShell {
buildInputs = packages;
shellHook = ''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
'';
};
});
in {
devShell = pkgs.mkShell {
buildInputs = packages;
shellHook = ''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
'';
};
});
}

View File

@@ -1,15 +1,11 @@
export interface IconsConfig {
codePoints: Record<string, string>
inputPath: string
outputPath: string
icons: string[]
}
const config: IconsConfig = {
/** @type {import('./src/tools/icons-config').IconsConfig} */
const config = {
inputPath:
"node_modules/@fontsource-variable/material-symbols-rounded/files/material-symbols-rounded-latin-full-normal.woff2",
outputPath: "src/lib/assets/icons.min.woff2",
icons: [
"adjust",
"add",
"piano",
"keyboard",
"settings",
@@ -25,6 +21,7 @@ const config: IconsConfig = {
"cable",
"person",
"sync",
"school",
"restart_alt",
"usb",
"usb_off",
@@ -59,6 +56,43 @@ const config: IconsConfig = {
"translate",
"play_arrow",
"extension",
"upload_file",
"commit",
"bug_report",
"delete",
"remove_selection",
"bolt",
"undo",
"redo",
"navigate_before",
"navigate_next",
"print",
"restore_from_trash",
"history",
"history_toggle_off",
"sentiment_satisfied",
"sentiment_dissatisfied",
"sentiment_very_satisfied",
"sentiment_neutral",
"sentiment_very_dissatisfied",
"sentiment_excited",
"sentiment_frustrated",
"sentiment_calm",
"sentiment_stressed",
"sentiment_extremely_dissatisfied",
"sentiment_sad",
"sentiment_content",
"sentiment_worried",
"timer",
"target",
"download",
"download_2",
"upload_2",
"stat_minus_2",
"stat_2",
"description",
"add_circle",
"refresh",
],
codePoints: {
speed: "e9e4",
@@ -70,6 +104,13 @@ const config: IconsConfig = {
counter_3: "f782",
ios_share: "e6b8",
light_mode: "e518",
upload_file: "e9fc",
no_sound: "e710",
sentiment_extremely_dissatisfied: "f194",
download_2: "f523",
upload_2: "ff52",
stat_minus_2: "e69c",
stat_2: "e699",
},
}

1425
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,16 @@
{
"name": "amacc1ng",
"version": "0.4.1",
"name": "charachorder-device-manager",
"version": "1.3.2",
"license": "AGPL-3.0-or-later",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/CharaChorder/DeviceManager.git"
},
"homepage": "https://docs.charachorder.com",
"bugs": {
"url": "https://github.com/CharaChorder/DeviceManager/issues"
},
"scripts": {
"dev": "npm-run-all --parallel vite typesafe-i18n",
"dev:tauri": "tauri dev",
@@ -15,61 +23,62 @@
"postinstall": "patch-package",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"minify-icons": "ts-node-esm src/tools/minify-icon-font.ts",
"minify-icons": "node src/tools/minify-icon-font.js",
"version": "node src/tools/version.js && git add src-tauri/Cargo.toml && git add src-tauri/tauri.conf.json",
"lint": "prettier --plugin-search-dir . --check .",
"format": "prettier --plugin-search-dir . --write .",
"typesafe-i18n": "typesafe-i18n"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.9.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/language": "^6.8.0",
"@codemirror/commands": "^6.2.5",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/language": "^6.9.0",
"@codemirror/state": "^6.2.1",
"@fontsource-variable/material-symbols-rounded": "^5.0.6",
"@fontsource-variable/noto-sans-mono": "^5.0.7",
"@fontsource-variable/material-symbols-rounded": "^5.0.16",
"@fontsource-variable/noto-sans-mono": "^5.0.17",
"@material/material-color-utilities": "^0.2.7",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.22.4",
"@sveltejs/vite-plugin-svelte": "^2.4.3",
"@sveltejs/kit": "^1.24.1",
"@sveltejs/vite-plugin-svelte": "^2.4.5",
"@tauri-apps/api": "^1.4.0",
"@tauri-apps/cli": "^1.4.0",
"@theaninova/prettier-config": "^1.0.0",
"@types/dom-view-transitions": "^1.0.1",
"@types/flexsearch": "^0.7.3",
"@types/w3c-web-serial": "^1.0.3",
"@vite-pwa/sveltekit": "^0.2.5",
"autoprefixer": "^10.4.14",
"@types/w3c-web-usb": "^1.0.10",
"@vite-pwa/sveltekit": "^0.2.7",
"autoprefixer": "^10.4.15",
"codemirror": "^6.0.1",
"cypress": "^12.17.3",
"cypress": "^13.1.0",
"flexsearch": "^0.7.31",
"fontkit": "^2.0.2",
"glob": "^10.3.3",
"glob": "^10.3.4",
"hotkeys-js": "^3.12.0",
"jsdom": "^22.1.0",
"npm-run-all": "^4.1.5",
"patch-package": "^8.0.0",
"prettier": "^3.0.1",
"prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3",
"sass": "^1.64.2",
"stylelint": "^15.10.2",
"stylelint-config-clean-order": "^5.0.1",
"sass": "^1.66.1",
"stylelint": "^15.10.3",
"stylelint-config-clean-order": "^5.2.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-prettier-scss": "^1.0.0",
"stylelint-config-recommended-scss": "^12.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"svelte": "^4.1.2",
"svelte-check": "^3.4.6",
"stylelint-config-recommended-scss": "^13.0.0",
"stylelint-config-standard-scss": "^11.0.0",
"svelte": "^4.2.0",
"svelte-check": "^3.5.1",
"svelte-preprocess": "^5.0.4",
"tippy.js": "^6.3.7",
"ts-node": "^10.9.1",
"typesafe-i18n": "^5.26.0",
"typescript": "^5.0.0",
"vite": "^4.4.8",
"typesafe-i18n": "^5.26.2",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-mkcert": "^1.16.0",
"vite-plugin-pwa": "^0.16.4",
"vitest": "^0.34.1"
"vite-plugin-pwa": "^0.17.4",
"vitest": "^0.34.4"
},
"type": "module",
"prettier": "@theaninova/prettier-config"
"type": "module"
}

2
src-tauri/Cargo.lock generated
View File

@@ -85,7 +85,7 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
[[package]]
name = "app"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"serde",
"serde_json",

View File

@@ -1,6 +1,6 @@
[package]
name = "app"
version = "0.4.1"
version = "1.3.2"
description = "A Tauri App"
authors = ["Thea Schöbl <dev@theaninova.de>"]
license = "AGPL-3"
@@ -18,7 +18,7 @@ tauri-build = { version = "1.4.0", features = [] }
serde_json = "1.0"
serialport = "4.2.1"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = ["updater"] }
tauri = { version = "1.4.0", features = ["updater", "devtools"] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

View File

@@ -6,21 +6,14 @@
"devPath": "http://localhost:5173",
"distDir": "../build"
},
"package": {
"productName": "amacc1ng",
"version": "0.4.1"
},
"package": { "productName": "amacc1ng", "version": "1.3.2" },
"tauri": {
"allowlist": {
"all": false
},
"allowlist": { "all": false },
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "AGPL-3.0-or-later",
"deb": {
"depends": []
},
"deb": { "depends": [] },
"externalBin": [],
"icon": [
"icons/32x32.png",
@@ -47,9 +40,7 @@
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"security": { "csp": null },
"updater": {
"active": true,
"endpoints": [

View File

@@ -1,8 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="icon" href="%sveltekit.assets%/icon.svg" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

5
src/env.d.ts vendored
View File

@@ -7,6 +7,11 @@ interface ImportMetaEnv {
readonly TAURI_ARCH?: string
readonly TAURI_DEBUG?: boolean
readonly TAURI_PLATFORM_TYPE?: string
readonly VITE_HOMEPAGE_URL: string
readonly VITE_BUGS_URL: string
readonly VITE_DOCS_URL: string
readonly VIET_LEARN_URL: string
}
interface ImportMeta {

View File

@@ -1,14 +1,52 @@
import type {Translation} from "../i18n-types"
const de = {
TITLE: "amaCC1ng",
TITLE: "CharaChorder Gerätemanager",
DESCRIPTION: "Gerätemanager und Konfigurationstool für CharaChorder Geräte.",
saveActions: {
UNDO: "Rückgängig (<kbd class='icon'>shift</kbd> halten um alle Änderungen rückgängig zu machen)",
REDO: "Wiederholen",
SAVE: "Speichern",
},
update: {
TITLE: "Gerät aktualisieren",
},
sync: {
TITLE_READ: "Neueste Änderungen werden abgerufen",
TITLE_WRITE: "Änderungen werden gespeichert",
RELOAD: "Neu laden",
},
backup: {
TITLE: "Sicherungskopie",
TITLE: "Verlauf speichern",
INDIVIDUAL: "Einzeldateien",
DISCLAIMER:
"Sicherungskopien verlassen unter keinen Umständen diesen Computer und werden nie mit uns geteilt oder auf Server hochgeladen.",
DOWNLOAD: "Kopie Speichern",
"Der Verlauf wird als Backup in diesem Browser gespeichert. Der Verlauf bleibt auf diesem Computer.",
DOWNLOAD: "Alles herunterladen",
RESTORE: "Wiederherstellen",
},
modal: {
CLOSE: "Schließen",
},
actionSearch: {
PLACEHOLDER: "Nach Aktionen suchen",
CURRENT_ACTION: "Aktuelle Aktion",
NEXT_ACTION: "Aktion nach dem nächsten Speichern",
DELETE: "Entfernen",
filter: {
ALL: "Alle",
},
LIVE_LAYOUT_INFO: "Diese Aktion wurde auf Basis des Systemtastaturlayouts ermittelt.",
SHIFT_WARNING: "Diese Aktion hält <kbd class='icon'>shift</kbd>",
ALT_CODE_WARNING: "Dieses Alt-Code Makro funktioniert nur unter Windows",
},
share: {
TITLE: "Teilen",
URL_COPIED: "Teilbare URL kopiert!",
EXTRA_DOWNLOAD: "Als Datei herunterladen",
},
print: {
TITLE: "Drucken",
},
profile: {
TITLE: "Profil",
LANGUAGE: "Sprache",
@@ -25,10 +63,15 @@ const de = {
CONNECT: "Verbinden",
DISCONNECT: "Entfernen",
TERMINAL: "Konsole",
APPLY_SETTINGS: "Änderungen auf das Gerät brennen",
NO_DEVICE: "Kein Gerät verbunden",
LINUX_PERMISSIONS:
"Auf den meisten Linux-basierten Systemen müssen zuerst Berechtigungen angepasst werden.",
bootMenu: {
TITLE: "Bootmenü",
REBOOT: "Neustarten",
BOOTLOADER: "Bootloader",
POWER_WARNING: "Um vom Bootloader aus neu zu starten muss das Gerät neu verbunden werden.",
},
},
browserWarning: {
@@ -41,14 +84,44 @@ const de = {
"Auch wenn alle Chromium-basieren Desktop Browser diese Voraussetzung grundsätzlich erfüllen, haben einige Browser ",
INFO_BROWSER_INFIX: "wie zum Beispiel Brave",
INFO_BROWSER_SUFFIX: " sich bewusst dazu entschieden die API zu deaktivieren.",
DOWNLOAD_APP: "Desktop-app herunterladen",
DOWNLOAD_APP:
"Chrome oder Edge werden offiziell unterstützt, andere Browser könnten aber auch funktionieren.",
},
changes: {
TITLE: "Änderungen importieren",
ALL_CHANGES: "Alle Änderungen",
layout: {
TITLE: "{0} veränderte Belegung{{:|en}}",
LAYER: "{changes} Belegung{{changes:|en}} in Ebene {layer} ändern",
},
settings: {
TITLE: "{0} Einstellung{{|en}} anpassen",
},
chords: {
TITLE: "{0} Akkorde",
NEW_CHORDS: "{0} neue Akkord{{|e}} hinzufügen",
CHANGED_CHORDS: "{0} Akkord{{|e}} ersetzen",
DELETED_CHORDS: "{0} Akkord{{|e}} zum löschen markieren",
},
},
configure: {
chords: {
TITLE: "Akkorde",
HOLD_KEYS: "Akkord halten",
NEW_CHORD: "Neuer Akkord",
DUPLICATE: "Akkord existiert bereits",
search: {
PLACEHOLDER: "{0} Akkord{{|e}} durchsuchen",
NO_RESULTS: "Keine Ergebnisse",
},
conflict: {
TITLE: "Akkordkonflikt",
DESCRIPTION:
"Der Akkord {0} würde einen bereits existierenden Akkord überschreiben. Wirklich fortfahren?",
CONFIRM: "Überschreiben",
ABORT: "Überspringen",
},
TRY_TYPING: "Versuche hier zu tippen",
},
layout: {
TITLE: "Layout",

View File

@@ -1,13 +1,51 @@
import type {BaseTranslation} from "../i18n-types"
const en = {
TITLE: "amaCC1ng",
TITLE: "CharaChorder Device Manager",
DESCRIPTION: "The device manager and configuration tool for CharaChorder devices.",
saveActions: {
UNDO: "Undo (hold <kbd class='icon'>shift</kbd> to undo all changes)",
REDO: "Redo",
SAVE: "Save",
},
update: {
TITLE: "Update your device",
},
backup: {
TITLE: "Local Backup",
DISCLAIMER: "Backups remain on your computer and are never shared with us or uploaded to our servers.",
DOWNLOAD: "Download Backup",
TITLE: "Store History",
INDIVIDUAL: "Individual backups",
DISCLAIMER: "Your history is stored as a backup in this browser. The history remains on your computer.",
DOWNLOAD: "Download Everything",
RESTORE: "Restore",
},
sync: {
TITLE_READ: "Reading latest changes",
TITLE_WRITE: "Saving changes to device",
RELOAD: "Reload",
},
modal: {
CLOSE: "Close",
},
actionSearch: {
PLACEHOLDER: "Search for actions",
CURRENT_ACTION: "Current action",
NEXT_ACTION: "Action after next save",
DELETE: "Remove",
filter: {
ALL: "All",
},
LIVE_LAYOUT_INFO: "This output was determined using on your system layout.",
SHIFT_WARNING: "This action holds <kbd class='icon'>shift</kbd>",
ALT_CODE_WARNING: "This alt-code macro only works on Windows",
},
share: {
TITLE: "Share",
URL_COPIED: "Sharable URL copied!",
EXTRA_DOWNLOAD: "Download as file",
},
print: {
TITLE: "Print",
},
profile: {
TITLE: "Profile",
LANGUAGE: "Language",
@@ -24,10 +62,14 @@ const en = {
CONNECT: "Connect",
DISCONNECT: "Disconnect",
TERMINAL: "Terminal",
APPLY_SETTINGS: "Flash changes to device",
NO_DEVICE: "No device connected",
LINUX_PERMISSIONS: "Most Linux based systems need adjusted permissions in order to connect your device.",
bootMenu: {
TITLE: "Boot Menu",
REBOOT: "Reboot",
BOOTLOADER: "Bootloader",
POWER_WARNING: "To reboot from bootloader you need to physically reconnect your device.",
},
},
browserWarning: {
@@ -39,14 +81,43 @@ const en = {
"Though all chromium-based desktop browsers fulfill this requirement, some derivations such as Brave ",
INFO_BROWSER_INFIX: "have been known to disable the API intentionally",
INFO_BROWSER_SUFFIX: ".",
DOWNLOAD_APP: "Download the desktop app",
DOWNLOAD_APP: "Chrome or Edge are officially supported, but other browsers might work as well.",
},
changes: {
TITLE: "Import changes",
ALL_CHANGES: "All changes",
layout: {
TITLE: "{0} layout change{{|s}}",
LAYER: "Update {changes} key{{changes:|s}} in layer {layer}",
},
settings: {
TITLE: "Update {0} setting{{|s}}",
},
chords: {
TITLE: "{0} chords",
NEW_CHORDS: "Add {0} new chord{{|s}}",
CHANGED_CHORDS: "Replace {0} chord{{|s}}",
DELETED_CHORDS: "Mark {0} chord{{|s}} for deletion",
},
},
configure: {
chords: {
TITLE: "Chords",
HOLD_KEYS: "Hold chord",
NEW_CHORD: "New chord",
DUPLICATE: "Chord already exists",
search: {
PLACEHOLDER: "Search {0} chord{{|s}}",
NO_RESULTS: "No results",
},
conflict: {
TITLE: "Chord conflict",
DESCRIPTION:
"Your chord {0} conflicts with an existing chord. Are you sure you want to overwrite this chord?",
CONFIRM: "Overwrite",
ABORT: "Skip",
},
TRY_TYPING: "Try typing here",
},
layout: {
TITLE: "Layout",

View File

@@ -1,11 +1,10 @@
import type { FormattersInitializer } from 'typesafe-i18n'
import type { Locales, Formatters } from './i18n-types'
import type {FormattersInitializer} from "typesafe-i18n"
import type {Locales, Formatters} from "./i18n-types"
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
const formatters: Formatters = {
// add your formatter functions here
}
const formatters: Formatters = {
// add your formatter functions here
}
return formatters
return formatters
}

View File

@@ -1,59 +0,0 @@
import type {Action} from "svelte/action"
import Index from "flexsearch"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import tippy from "tippy.js"
import ActionAutocomplete from "$lib/components/ActionAutocomplete.svelte"
import {browser} from "$app/environment"
const index = browser ? new Index({tokenize: "full"}) : undefined
for (const action of Object.values(KEYMAP_CODES)) {
index?.add(
action.code,
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
action.description || ""
}`,
)
}
const exact = Object.fromEntries(
Object.values(KEYMAP_CODES)
.filter(it => !!it.id)
.map(it => [it.id, it] as const),
)
export const actionAutocomplete: Action<HTMLInputElement> = node => {
if (!browser) return
let completionComponent: ActionAutocomplete
const completionDialog = tippy(node, {
interactive: true,
placement: "bottom-start",
hideOnClick: false,
theme: "surface-variant search-completion",
arrow: false,
trigger: "focus",
offset: [0, 0],
onCreate(instance) {
const target = instance.popper.querySelector(".tippy-content")!
completionComponent = new ActionAutocomplete({target, props: {width: node.clientWidth}})
},
onDestroy() {
completionComponent.$destroy()
},
})
function input(event: Event) {
completionComponent.$set({
results: index!.search(node.value),
exact: exact[node.value],
code: Number(node.value),
})
}
node.addEventListener("input", input)
return {
destroy() {
node.removeEventListener("input", input)
},
}
}

View File

@@ -0,0 +1,141 @@
name: ASCII Macros
description: ASCII Characters that are macros for SHFT + key
actions:
33:
id: "!"
title: Exclamation Point
34:
id: '"'
title: Double Quote
35:
id: "#"
title: Hash Symbol
36:
id: "$"
title: Dollar Sign
37:
id: "%"
title: Percent
38:
id: "&"
title: Ampersand
40:
id: "("
title: Opening Parenthesis
41:
id: ")"
title: Closing Parenthesis
42:
id: "*"
title: Asterisk
58:
id: ":"
title: Colon
60:
id: "<"
title: Less Than
62:
id: ">"
title: Greater Than
63:
id: "?"
title: Question Mark
64:
id: "@"
title: At Symbol
65:
id: "A"
title: Uppercase A
66:
id: "B"
title: Uppercase B
67:
id: "C"
title: Uppercase C
68:
id: "D"
title: Uppercase D
69:
id: "E"
title: Uppercase E
70:
id: "F"
title: Uppercase F
71:
id: "G"
title: Uppercase G
72:
id: "H"
title: Uppercase H
73:
id: "I"
title: Uppercase I
74:
id: "J"
title: Uppercase J
75:
id: "K"
title: Uppercase K
76:
id: "L"
title: Uppercase L
77:
id: "M"
title: Uppercase M
78:
id: "N"
title: Uppercase N
79:
id: "O"
title: Uppercase O
80:
id: "P"
title: Uppercase P
81:
id: "Q"
title: Uppercase Q
82:
id: "R"
title: Uppercase R
83:
id: "S"
title: Uppercase S
84:
id: "T"
title: Uppercase T
85:
id: "U"
title: Uppercase U
86:
id: "V"
title: Uppercase V
87:
id: "W"
title: Uppercase W
88:
id: "X"
title: Uppercase X
89:
id: "Y"
title: Uppercase Y
90:
id: "Z"
title: Uppercase Z
94:
id: "^"
title: Caret
95:
id: "_"
title: Underscore
123:
id: "{"
title: Left Curly Brace
124:
id: "|"
title: Pipe
125:
id: "}"
title: Right Curly Brace
126:
id: "~"
title: Tilde

View File

@@ -7,39 +7,9 @@ actions:
description: |
While SPACE is used for keymaps and chord, just a " " is used in chord outputs.
This action is unique in this way. Technically it is "printable", but it is not visible.
33:
id: "!"
title: Exclamation Point
34:
id: '"'
title: Double Quote
35:
id: "#"
title: Hash Symbol
36:
id: "$"
title: Dollar Sign
37:
id: "%"
title: Percent
38:
id: "&"
title: Ampersand
39:
id: "'"
title: Single Quote
40:
id: "("
title: Opening Parenthesis
41:
id: ")"
title: Closing Parenthesis
42:
id: "*"
title: Asterisk
43:
id: "+"
title: Plus
44:
id: ","
title: Comma
@@ -82,105 +52,12 @@ actions:
57:
id: "9"
title: Nine
58:
id: ":"
title: Colon
59:
id: ";"
title: Semicolon
60:
id: "<"
title: Less Than
61:
id: "="
title: Equals
62:
id: ">"
title: Greater Than
63:
id: "?"
title: Question Mark
64:
id: "@"
title: At Symbol
65:
id: "A"
title: Uppercase A
66:
id: "B"
title: Uppercase B
67:
id: "C"
title: Uppercase C
68:
id: "D"
title: Uppercase D
69:
id: "E"
title: Uppercase E
70:
id: "F"
title: Uppercase F
71:
id: "G"
title: Uppercase G
72:
id: "H"
title: Uppercase H
73:
id: "I"
title: Uppercase I
74:
id: "J"
title: Uppercase J
75:
id: "K"
title: Uppercase K
76:
id: "L"
title: Uppercase L
77:
id: "M"
title: Uppercase M
78:
id: "N"
title: Uppercase N
79:
id: "O"
title: Uppercase O
80:
id: "P"
title: Uppercase P
81:
id: "Q"
title: Uppercase Q
82:
id: "R"
title: Uppercase R
83:
id: "S"
title: Uppercase S
84:
id: "T"
title: Uppercase T
85:
id: "U"
title: Uppercase U
86:
id: "V"
title: Uppercase V
87:
id: "W"
title: Uppercase W
88:
id: "X"
title: Uppercase X
89:
id: "Y"
title: Uppercase Y
90:
id: "Z"
title: Uppercase Z
91:
id: "["
title: Left Bracket
@@ -190,12 +67,6 @@ actions:
93:
id: "]"
title: Right Bracket
94:
id: "^"
title: Caret
95:
id: "_"
title: Underscore
96:
id: "`"
title: Backtick
@@ -277,19 +148,6 @@ actions:
122:
id: "z"
title: Lowercase z
123:
id: "{"
title: Left Curly Brace
124:
id: "|"
title: Pipe
125:
id: "}"
title: Right Curly Brace
126:
id: "~"
title: Tilde
127:
id: "DEL"
title: Delete
icon: delete_forever

View File

@@ -6,34 +6,73 @@ type: unassigned
actions:
600:
id: "LH_THUMB_3_3D"
title: Left Hand Thumb Top 3D Click
title: "Left Hand Thumb Bottom 3D Click"
icon: "adjust"
601:
id: "LH_THUMB_2_3D"
title: Left Hand Thumb Middle 3D Click
title: "Left Hand Thumb Middle 3D Click"
icon: "adjust"
602:
id: "LH_THUMB_1_3D"
title: Left Hand Thumb Bottom 3D Click
title: "Left Hand Thumb Top 3D Click"
icon: "adjust"
603:
id: "LH_INDEX_3D"
title: Left Hand Index Finger 3D Click
title: "Left Hand Index Finger 3D Click"
icon: "adjust"
604:
id: "LH_MID_1_3D"
title: Left Hand Middle Finger 3D Click
title: "Left Hand Middle Finger 3D Click"
icon: "adjust"
605:
id: "LH_RING_1_3D"
title: Left Hand Ring Finger 3D Click
title: "Left Hand Ring Finger 3D Click"
icon: "adjust"
606:
id: "LH_PINKY_3D"
title: Left Hand Pinky 3D Click,
# TODO...
# ["607", "CharaChorder One", "LH_MID_2_3D", "", ""],
# ["608", "CharaChorder One", "LH_RING_2_3D", "", ""],
# ["609", "CharaChorder One", "RH_THUMB_3_3D", "", ""],
# ["610", "CharaChorder One", "RH_THUMB_2_3D", "", ""],
# ["611", "CharaChorder One", "RH_THUMB_1_3D", "", ""],
# ["612", "CharaChorder One", "RH_INDEX_3D", "", ""],
# ["613", "CharaChorder One", "RH_MID_1_3D", "", ""],
# ["614", "CharaChorder One", "RH_RING_1_3D", "", ""],
# ["615", "CharaChorder One", "RH_PINKY_3D", "", ""],
# ["616", "CharaChorder One", "RH_MID_2_3D", "", ""],
# ["617", "CharaChorder One", "RH_RING_2_3D", "", ""]
title: "Left Hand Pinky 3D Click"
icon: "adjust"
607:
id: "LH_MID_2_3D"
title: "Left Hand Middle Finger 2 3D Click"
icon: "adjust"
608:
id: "LH_RING_2_3D"
title: "Left Hand Ring Finger 2 3D Click"
icon: "adjust"
609:
id: "RH_THUMB_3_3D"
title: "Right Hand Thumb Bottom 3D Click"
icon: "adjust"
610:
id: "RH_THUMB_2_3D"
title: "Right Hand Thumb Middle 3D Click"
icon: "adjust"
611:
id: "RH_THUMB_1_3D"
title: "Right Hand Thumb Top 3D Click"
icon: "adjust"
612:
id: "RH_INDEX_3D"
title: "Right Hand Index Finger 3D Click"
icon: "adjust"
613:
id: "RH_MID_1_3D"
title: "Right Hand Middle Finger 3D Click"
icon: "adjust"
614:
id: "RH_RING_1_3D"
title: "Right Hand Ring Finger 3D Click"
icon: "adjust"
615:
id: "RH_PINKY_3D"
title: "Right Hand Pinky 3D Click"
icon: "adjust"
616:
id: "RH_MID_2_3D"
title: "Right Hand Middle Finger 2 3D Click"
icon: "adjust"
617:
id: "RH_RING_2_3D"
title: "Right Hand Ring Finger 2 3D Click"
icon: "adjust"

View File

@@ -26,7 +26,7 @@ actions:
536:
id: "DUP"
title: Repeat Last Note
icon: control_point_duplicate
icon: copy_all
description: |
In character entry, it repeats your last input.
In chorded entry, it is used for words with repeating letters.

View File

@@ -4,41 +4,52 @@ icon: keyboard
actions:
512: &left_ctrl
id: "LEFT_CTRL"
display: CTRL
title: Control Keyboard Modifier
keyCode: ControlLeft
variant: left
icon: keyboard_control_key
513: &left_shift
id: "LEFT_SHIFT"
title: Shift Keyboard Modifier
keyCode: ShiftLeft
variant: left
icon: shift
514: &left_alt
id: "LEFT_ALT"
display: ALT
title: Alt Keyboard Modifier
keyCode: AltLeft
variant: left
icon: keyboard_option_key
515: &left_gui
id: "LEFT_GUI"
title: GUI Keyboard Modifier
keyCode: MetaLeft
icon: apps
variant: left
icon: keyboard_command_key
516:
variationOf: 512
<<: *left_ctrl
id: "RIGHT_CTRL"
keyCode: ControlRight
variant: right
517:
variationOf: 513
<<: *left_shift
id: "RIGHT_SHIFT"
keyCode: ShiftRight
variant: right
518:
variationOf: 514
<<: *left_alt
id: "RIGHT_ALT"
keyCode: AltRight
variant: right
519:
variationOf: 515
<<: *left_gui
id: "RIGHT_GUI"
keyCode: MetaRight
variant: right
520:
id: "RELEASE_MOD"
title: Release all keyboard modifiers
@@ -51,3 +62,11 @@ actions:
id: "RELEASE_KEYS"
title: Release all keys, but not keyboard modifiers
icon: text_rotate_up
523:
id: "PRESS_NEXT"
title: "Press and do not release the next key/action"
icon: download
524:
id: "RELEASE_NEXT"
title: "Release the next key/action in the sequence"
icon: upload

View File

@@ -2,6 +2,7 @@ export interface KeymapCategory {
name: string
description: string
icon?: string
display?: string
type?: "unassigned"
actions: Record<number, Partial<ActionInfo>>
}
@@ -10,7 +11,9 @@ export interface ActionInfo {
id: string
title: string
icon: string
display: string
description: string
variant: "left" | "right"
variantOf: number
keyCode: string
}

View File

@@ -1,9 +1,11 @@
name: Raw Scancodes
description: Raw Keyboard Scancodes
name: Key codes
description: OS-Layout sensitive keycodes
actions:
256:
id: "KSC_00"
icon: block
title: No Key Pressed
description: Also commonly used at the end of a chord to remove auto-spaces
257:
id: "KSC_01"
title: Keyboard Error Roll Over
@@ -15,311 +17,408 @@ actions:
title: Keyboard Error Undefined
260:
id: "KEY_A"
keyCode: "KeyA"
title: Keyboard a and A (US English)
description: Non US English keyboard users may prefer these Raw Scancodes
261:
id: "KEY_B"
keyCode: "KeyB"
title: Keyboard b and B (US English)
262:
id: "KEY_C"
keyCode: "KeyC"
title: Keyboard c and C (US English)
263:
id: "KEY_D"
keyCode: "KeyD"
title: Keyboard d and D (US English)
264:
id: "KEY_E"
keyCode: "KeyE"
title: Keyboard e and E (US English)
265:
id: "KEY_F"
keyCode: "KeyF"
title: Keyboard f and F (US English)
266:
id: "KEY_G"
keyCode: "KeyG"
title: Keyboard g and G (US English)
267:
id: "KEY_H"
keyCode: "KeyH"
title: Keyboard h and H (US English)
268:
id: "KEY_I"
keyCode: "KeyI"
title: Keyboard i and I (US English)
269:
id: "KEY_J"
keyCode: "KeyJ"
title: Keyboard j and J (US English)
270:
id: "KEY_K"
keyCode: "KeyK"
title: Keyboard k and K (US English)
271:
id: "KEY_L"
keyCode: "KeyL"
title: Keyboard l and L (US English)
272:
id: "KEY_M"
keyCode: "KeyM"
title: Keyboard m and M (US English)
273:
id: "KEY_N"
keyCode: "KeyN"
title: Keyboard n and N (US English)
274:
id: "KEY_O"
keyCode: "KeyO"
title: Keyboard o and O (US English)
275:
id: "KEY_P"
keyCode: "KeyP"
title: Keyboard p and P (US English)
276:
id: "KEY_Q"
keyCode: "KeyQ"
title: Keyboard q and Q (US English)
277:
id: "KEY_R"
keyCode: "KeyR"
title: Keyboard r and R (US English)
278:
id: "KEY_S"
keyCode: "KeyS"
title: Keyboard s and S (US English)
279:
id: "KEY_T"
keyCode: "KeyT"
title: Keyboard t and T (US English)
280:
id: "KEY_U"
keyCode: "KeyU"
title: Keyboard u and U (US English)
281:
id: "KEY_V"
keyCode: "KeyV"
title: Keyboard v and V (US English)
282:
id: "KEY_W"
keyCode: "KeyW"
title: Keyboard w and W (US English)
283:
id: "KEY_X"
keyCode: "KeyX"
title: Keyboard x and X (US English)
284:
id: "KEY_Y"
keyCode: "KeyY"
title: Keyboard y and Y (US English)
285:
id: "KEY_Z"
keyCode: "KeyZ"
title: Keyboard z and Z (US English)
286:
id: "KEY_1"
keyCode: "Digit1"
title: Keyboard 1 and ! (US English)
287:
id: "KEY_2"
keyCode: "Digit2"
title: Keyboard 2 and @ (US English)
288:
id: "KEY_3"
keyCode: "Digit3"
title: Keyboard 3 and # (US English)
289:
id: "KEY_4"
keyCode: "Digit4"
title: Keyboard 4 and $ (US English)
290:
id: "KEY_5"
keyCode: "Digit5"
title: Keyboard 5 and % (US English)
291:
id: "KEY_6"
keyCode: "Digit6"
title: Keyboard 6 and ^ (US English)
292:
id: "KEY_7"
keyCode: "Digit7"
title: Keyboard 7 and & (US English)
293:
id: "KEY_8"
keyCode: "Digit8"
title: Keyboard 8 and * (US English)
294:
id: "KEY_9"
keyCode: "Digit9"
title: Keyboard 9 and ( (US English)
295:
id: "KEY_0"
keyCode: "Digit0"
title: Keyboard 0 and ) (US English)
296:
id: "ENTER"
keyCode: "Enter"
title: Keyboard Return (US English)
icon: keyboard_return
297:
id: "ESC"
keyCode: "Escape"
title: Keyboard Escape (US English)
298:
id: "BKSP"
keyCode: "Backspace"
title: Keyboard Backspace (US English)
icon: backspace
299:
id: "TAB"
keyCode: "Tab"
title: Keyboard Tab (US English)
icon: keyboard_tab
300:
id: "KSC_2C"
keyCode: "Space"
title: Keyboard Space (US English)
description: |
The ASCII space is preferred over this raw scancode for the space bar.
icon: space_bar
301:
id: "KSC_2D"
keyCode: "Minus"
title: Keyboard - and _ (US English)
302:
id: "KSC_2E"
keyCode: "Equal"
title: Keyboard = and + (US English)
303:
id: "KSC_2F"
keyCode: "BracketLeft"
title: Keyboard [ and { (US English)
304:
id: "KSC_30"
keyCode: "BracketRight"
title: Keyboard ] and } (US English)
305:
id: "KSC_31"
keyCode: "Backslash"
title: Keyboard \ and | (US English)
306:
id: "KSC_32"
# TODO: also backslash?
title: Keyboard Non-US \# and ~ (US English)
307:
id: "KSC_33"
keyCode: "Semicolon"
title: "Keyboard ; and : (US English)"
308:
id: "KSC_34"
keyCode: "Quote"
title: Keyboard ' and " (US English)
309:
id: "KSC_35"
keyCode: "Backquote"
title: Keyboard ` and ~ (US English)
310:
id: "KSC_36"
keyCode: "Comma"
title: Keyboard , and < (US English)
311:
id: "KSC_37"
keyCode: "Period"
title: Keyboard . and > (US English)
312:
id: "KSC_38"
keyCode: "Slash"
title: Keyboard / and ? (US English)
313:
id: "CAPSLOCK"
keyCode: "CapsLock"
title: Keyboard Caps Lock
icon: shift_lock
314:
id: "F1"
keyCode: "F1"
title: Keyboard F1
315:
id: "F2"
keyCode: "F2"
title: Keyboard F2
316:
id: "F3"
keyCode: "F3"
title: Keyboard F3
317:
id: "F4"
keyCode: "F4"
title: Keyboard F4
318:
id: "F5"
keyCode: "F5"
title: Keyboard F5
319:
id: "F6"
keyCode: "F6"
title: Keyboard F6
320:
id: "F7"
keyCode: "F7"
title: Keyboard F7
321:
id: "F8"
keyCode: "F8"
title: Keyboard F8
322:
id: "F9"
keyCode: "F9"
title: Keyboard F9
323:
id: "F10"
keyCode: "F10"
title: Keyboard F10
324:
id: "F11"
keyCode: "F11"
title: Keyboard F11
325:
id: "F12"
keyCode: "F12"
title: Keyboard F12
326:
id: "PRTSCN"
keyCode: "PrintScreen"
title: Keyboard Print Screen
icon: screenshot_monitor
327:
id: "SCRLK"
keyCode: "ScrollLock"
title: Keyboard Scroll Lock
328:
id: "PAUSE"
keyCode: "Pause"
title: Keyboard Pause
329:
id: "INSERT"
keyCode: "Insert"
title: Keyboard Insert
icon: insert_text
330:
id: "HOME"
keyCode: "Home"
title: Keyboard Home
icon: home
331:
id: "PGUP"
keyCode: "PageUp"
title: Keyboard Page Up
icon: move_up
332:
id: "DELETE"
keyCode: "Delete"
title: Keyboard Delete Forward
333:
id: "END"
keyCode: "End"
title: Keyboard End
334:
id: "PGDN"
keyCode: "PageDown"
title: Keyboard Page Down
icon: move_down
335:
id: "ARROW_RT"
keyCode: "ArrowRight"
title: Keyboard Right Arrow
icon: keyboard_arrow_right
336:
id: "ARROW_LF"
keyCode: "ArrowLeft"
title: Keyboard Left Arrow
icon: keyboard_arrow_left
337:
id: "ARROW_DN"
keyCode: "ArrowDown"
title: Keyboard Down Arrow
icon: keyboard_arrow_down
338:
id: "ARROW_UP"
keyCode: "ArrowUp"
title: Keyboard Up Arrow
icon: keyboard_arrow_up
339:
id: "NUMLOCK"
keyCode: "NumLock"
title: Keyboard Num Lock and Clear
340:
id: "KP_SLASH"
keyCode: "NumpadDivide"
title: Keypad /
341:
id: "KP_ASTER"
keyCode: "NumpadStar"
title: Keypad *
342:
id: "KP_MINUS"
keyCode: "NumpadSubtract"
title: Keypad -
343:
id: "KP_PLUS"
keyCode: "NumpadAdd"
title: Keypad +
344:
id: "KP_ENTER"
keyCode: "NumpadEnter"
title: Keypad Enter
345:
id: "KP_1"
keyCode: "Numpad1"
title: Keypad 1 and End
346:
id: "KP_2"
keyCode: "Numpad2"
title: Keypad 2 and Down Arrow
347:
id: "KP_3"
keyCode: "Numpad3"
title: Keypad 3 and Page Down
348:
id: "KP_4"
keyCode: "Numpad4"
title: Keypad 4 and Left Arrow
349:
id: "KP_5"
keyCode: "Numpad5"
title: Keypad 5
350:
id: "KP_6"
keyCode: "Numpad6"
title: Keypad 6 and Rigth Arrow
351:
id: "KP_7"
keyCode: "Numpad7"
title: Keypad 7 and Home
352:
id: "KP_8"
keyCode: "Numpad8"
title: Keypad 8 and Up Arrow
353:
id: "KP_9"
keyCode: "Numpad9"
title: Keypad 9 and Page Up
354:
id: "KP_0"
keyCode: "Numpad0"
title: Keypad 0 and Insert
355:
id: "KP_DOT"
keyCode: "NumpadDecimal"
title: Keypad . and Delete
356:
id: "KSC_64"
keyCode: "IntlBackslash"
title: Keyboard Non-US \ and | (US English)
357:
id: "COMPOSE"
@@ -327,10 +426,12 @@ actions:
description: Officially supported by Win, Unix, and Boot
358:
id: "POWER"
keyCode: "Power"
title: Keyboard Power
description: Only officially supported by Mac and Unix
359:
id: "KP_EQUAL"
keyCode: "NumpadEqual"
title: Keypad =
description: Only officially supported by Mac
360:
@@ -787,10 +888,12 @@ actions:
description: Not required to be supported by any OS
472:
id: "KSC_D8"
keyCode: "NumpadClear"
title: Keypad Clear
description: Not required to be supported by any OS
473:
id: "KSC_D9"
keyCode: "NumpadClearEntry"
title: Keypad Clear Entry
description: Not required to be supported by any OS
474:
@@ -841,34 +944,42 @@ actions:
title: Keyboard Right GUI
488:
id: "KSC_E8"
keyCode: "MediaPlayPause"
title: Media Play Pause
description: Not required to be supported by any OS. Possibly deprecated.
489:
id: "KSC_E9"
keyCode: "MediaStop"
title: Media Stop CD
description: Not required to be supported by any OS. Possibly deprecated.
490:
id: "KSC_EA"
keyCode: "MediaTrackPrevious"
title: Media Previous Song
description: Not required to be supported by any OS. Possibly deprecated.
491:
id: "KSC_EB"
keyCode: "MediaTrackNext"
title: Media Next Song
description: Not required to be supported by any OS. Possibly deprecated.
492:
id: "KSC_EC"
keyCode: "Eject"
title: Media Eject CD
description: Not required to be supported by any OS. Possibly deprecated.
493:
id: "KSC_ED"
keyCode: "AudioVolumeUp"
title: Media Volume Up
description: Not required to be supported by any OS. Possibly deprecated.
494:
id: "KSC_EE"
keyCode: "AudioVolumeDown"
title: Media Volume Down
description: Not required to be supported by any OS. Possibly deprecated.
495:
id: "KSC_EF"
keyCode: "AudioVolumeMute"
title: Media Mute
description: Not required to be supported by any OS. Possibly deprecated.
496:
@@ -877,18 +988,22 @@ actions:
description: Not required to be supported by any OS. Possibly deprecated.
497:
id: "KSC_F1"
keyCode: "BrowserBack"
title: Media Back
description: Not required to be supported by any OS. Possibly deprecated.
498:
id: "KSC_F2"
keyCode: "BrowserForward"
title: Media Forward
description: Not required to be supported by any OS. Possibly deprecated.
499:
id: "KSC_F3"
keyCode: "BrowserStop"
title: Media Stop
description: Not required to be supported by any OS. Possibly deprecated.
500:
id: "KSC_F4"
keyCode: "BrowserSearch"
title: Media Find
description: Not required to be supported by any OS. Possibly deprecated.
501:
@@ -905,14 +1020,17 @@ actions:
description: Not required to be supported by any OS. Possibly deprecated.
504:
id: "KSC_F8"
keyCode: "Sleep"
title: Media Sleep
description: Not required to be supported by any OS. Possibly deprecated.
505:
id: "KSC_F9"
keyCode: "WakeUp"
title: Media Coffee
description: Not required to be supported by any OS. Possibly deprecated.
506:
id: "KSC_FA"
keyCode: "BrowserRefresh"
title: Media Refresh
description: Not required to be supported by any OS. Possibly deprecated.
507:

View File

@@ -1,36 +0,0 @@
name: CC1
col:
- gap: 156
row:
- row:
- {d: 30, e: 31, n: 32, w: 33, s: 34}
- col:
- {d: 25, e: 26, n: 27, w: 28, s: 29}
- {d: 40, e: 41, n: 42, w: 43, s: 44}
- col:
- {d: 20, e: 21, n: 22, w: 23, s: 24}
- {d: 35, e: 36, n: 37, w: 38, s: 39}
- {d: 15, e: 16, n: 17, w: 18, s: 19}
- row:
- {d: 60, w: 61, n: 62, e: 63, s: 64}
- col:
- {d: 65, w: 66, n: 67, e: 68, s: 69}
- {d: 80, w: 81, n: 82, e: 83, s: 84}
- col:
- {d: 70, w: 71, n: 72, e: 73, s: 74}
- {d: 85, w: 86, n: 87, e: 88, s: 89}
- {d: 75, w: 76, n: 77, e: 78, s: 79}
- gap: 48
margin-top: -32
row:
- {d: 10, e: 11, n: 12, w: 13, s: 14}
- {d: 55, w: 56, n: 57, e: 58, s: 59}
- gap: 160
row:
- {d: 5, e: 6, n: 7, w: 8, s: 9}
- {d: 50, w: 51, n: 52, e: 53, s: 54}
- gap: 320
margin-top: -12
row:
- {d: 0, e: 1, n: 2, w: 3, s: 4}
- {d: 45, w: 46, n: 47, e: 48, s: 49}

View 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

View File

@@ -0,0 +1,87 @@
name: Lite
col:
- row:
- key: 53
- key: 54
- key: 55
- key: 56
- key: 57
- key: 58
- key: 59
- key: 60
- key: 61
- key: 62
- key: 63
- key: 64
- key: 65
- key: 66
size: [ 2, 1 ]
- row:
- key: 39
size: [ 1.5, 1 ]
- key: 40
- key: 41
- key: 42
- key: 43
- key: 44
- key: 45
- key: 46
- key: 47
- key: 48
- key: 49
- key: 50
- key: 51
- key: 52
size: [ 1.5, 1 ]
- row:
- key: 26
size: [ 1.75, 1 ]
- key: 27
- key: 28
- key: 29
- key: 30
- key: 31
- key: 32
- key: 33
- key: 34
- key: 35
- key: 36
- key: 37
- key: 38
size: [ 2.25, 1 ]
- row:
- key: 12
size: [ 2, 1 ]
- key: 13
- key: 14
- key: 15
- key: 16
- key: 17
- key: 18
- key: 19
- key: 20
- key: 21
- key: 22
- key: 23
- key: 24
- key: 25
- row:
- key: 0
- key: 1
size: [ 1.25, 1 ]
- key: 2
size: [ 1.25, 1 ]
- key: 3
size: [ 2, 1 ]
- key: 4
- key: 5
- key: 6
size: [ 2, 1 ]
- key: 7
size: [ 1.25, 1 ]
- key: 8
size: [ 1.25, 1 ]
- key: 9
- key: 10
- key: 11

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

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<mask id="cross" maskUnits="userSpaceOnUse">
<rect x="0" y="0" width="32" height="32" fill="white" />
<path d="M0 0L32 32M0 32L32 0" stroke="black" stroke-width="3" />
</mask>
<circle cx="16" cy="16" r="11.5" fill="none" stroke="white" stroke-width="9" mask="url(#cross)" />
</svg>

Before

Width:  |  Height:  |  Size: 433 B

118
src/lib/assets/settings.yml Normal file
View File

@@ -0,0 +1,118 @@
settings:
0x1:
title: Enable Serial Header
description: boolean 0 or 1, default is 0
0x2:
title: Enable Serial Logging
description: boolean 0 or 1, default is 0
0x3:
title: Enable Serial Debugging
description: boolean 0 or 1, default is 0
0x4:
title: Enable Serial Raw
description: boolean 0 or 1, default is 0
0x5:
title: Enable Serial Chord
description: boolean 0 or 1, default is 0
0x6:
title: Enable Serial Keyboard
description: boolean 0 or 1, default is 0
0x7:
title: Enable Serial Mouse
description: boolean 0 or 1, default is 0
0x11:
title: Enable USB HID Keyboard
description: boolean 0 or 1, default is 1
0x12:
title: Enable Character Entry
description: boolean 0 or 1
0x13:
title: GUI-CTRL Swap Mode
description: boolean 0 or 1; 1 swaps keymap 0 and 1. (CCL only)
0x14:
title: Key Scan Duration
description: scan rate described in milliseconds; default is 2ms = 500Hz
0x15:
title: Key Debounce Press Duration
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
0x16:
title: Key Debounce Release Duration
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite
0x17:
title: Keyboard Output Character Microsecond Delays
description: delay time in microseconds (one delay for press and again for release); default is 480us; max is 10240us; increments of 40us
0x21:
title: Enable USB HID Mouse
description: boolean 0 or 1; default is 1
0x22:
title: Slow Mouse Speed
description: pixels to move at the mouse poll rate; default for CC1 is 5 = 250px/s
0x23:
title: Fast Mouse Speed
description: pixels to move at the mouse poll rate; default for CC1 is 25 = 1250px/s
0x24:
title: Enable Active Mouse
description: boolean 0 or 1; moves mouse back and forth every 60s
0x25:
title: Mouse Scroll Speed
description: default is 1; polls at 1/4th the rate of the mouse move updates
0x26:
title: Mouse Poll Duration
description: poll rate described in milliseconds; default is 20ms = 50Hz
0x31:
title: Enable Chording
description: boolean 0 or 1
0x32:
title: Enable Chording Character Counter Timeout
description: boolean 0 or 1; default is 1
0x33:
title: Chording Character Counter Timeout Timer
description: 0-255 deciseconds; default is 40 or 4.0 seconds
0x34:
title: Chord Detection Press Tolerance(ms)
description: 1-50 milliseconds
0x35:
title: Chord Detection Release Tolerance(ms)
description: 1-50 milliseconds
0x41:
title: Enable Spurring
description: boolean 0 or 1; default is 1
0x42:
title: Enable Spurring Character Counter Timeout
description: boolean 0 or 1; default is 1
0x43:
title: Spurring Character Counter Timeout Timer
description: 0-255 seconds; default is 240
0x51:
title: Enable Arpeggiates
description: boolean 0 or 1; default is 1
0x54:
title: Arpeggiate Tolerance
description: in milliseconds; default 800ms
0x61:
title: Enable Compound Chording (coming soon)
description: boolean 0 or 1; default is 0
0x64:
title: Compound Tolerance
description: in milliseconds; default 1500ms
0x81:
title: LED Brightness
description: 0-50 (CCL only); default is 5, which draws around 100 mA of current
0x82:
title: LED Color Code
description: Color Codes to be listed (CCL only)
0x83:
title: Enable LED Key Highlight (coming soon)
description: boolean 0 or 1 (CCL only)
0x84:
title: Enable LEDs
description: boolean 0 or 1; default is 1 (CCL only)
0x91:
title: Operating System
description: Operating system codes listed below
0x92:
title: Enable Realtime Feedback
description: boolean 0 or 1; default is 1
0x93:
title: Enable CharaChorder Ready on startup
description: boolean 0 or 1; default is 1

164
src/lib/backup/backup.ts Normal file
View File

@@ -0,0 +1,164 @@
import type {
CharaBackupFile,
CharaChordFile,
CharaFile,
CharaLayoutFile,
CharaSettingsFile,
} from "$lib/share/chara-file.js"
import type {Change} from "$lib/undo-redo.js"
import {changes, ChangeType, chords, layout, settings} from "$lib/undo-redo.js"
import {get} from "svelte/store"
import {serialPort} from "../serial/connection"
import {csvLayoutToJson, isCsvLayout} from "$lib/backup/compat/legacy-layout"
import {isCsvChords, csvChordsToJson} from "./compat/legacy-chords"
export function downloadFile<T extends CharaFile<string>>(contents: T) {
const downloadUrl = URL.createObjectURL(new Blob([JSON.stringify(contents)], {type: "application/json"}))
const element = document.createElement("a")
element.setAttribute(
"download",
`${contents.type}-${get(serialPort)?.device}-${new Date().toISOString()}.json`,
)
element.href = downloadUrl
element.setAttribute("target", "_blank")
element.click()
URL.revokeObjectURL(downloadUrl)
}
export function downloadBackup() {
downloadFile<CharaBackupFile>({
charaVersion: 1,
type: "backup",
history: [[createChordBackup(), createLayoutBackup(), createSettingsBackup()]],
})
}
export function createLayoutBackup(): CharaLayoutFile {
return {
charaVersion: 1,
type: "layout",
device: get(serialPort)?.device,
layout: get(layout).map(it => it.map(it => it.action)) as [number[], number[], number[]],
}
}
export function createChordBackup(): CharaChordFile {
return {charaVersion: 1, type: "chords", chords: get(chords).map(it => [it.actions, it.phrase])}
}
export function createSettingsBackup(): CharaSettingsFile {
return {charaVersion: 1, type: "settings", settings: get(settings).map(it => it.value)}
}
export async function restoreBackup(event: Event) {
const input = (event.target as HTMLInputElement).files![0]
if (!input) return
const text = await input.text()
if (input.name.endsWith(".json")) {
restoreFromFile(JSON.parse(text))
} else if (isCsvLayout(text)) {
restoreFromFile(csvLayoutToJson(text))
} else if (isCsvChords(text)) {
restoreFromFile(csvChordsToJson(text))
} else {
alert("Unknown backup format")
}
}
export function restoreFromFile(
file: CharaBackupFile | CharaSettingsFile | CharaLayoutFile | CharaChordFile,
) {
if (file.charaVersion !== 1) throw new Error("Incompatible backup")
switch (file.type) {
case "backup": {
const recent = file.history[0]
if (recent[1].device !== get(serialPort)?.device) {
alert("Backup is incompatible with this device")
throw new Error("Backup is incompatible with this device")
}
changes.update(changes => {
changes.push(
...getChangesFromChordFile(recent[0]),
...getChangesFromLayoutFile(recent[1]),
...getChangesFromSettingsFile(recent[2]),
)
return changes
})
break
}
case "chords": {
changes.update(changes => {
changes.push(...getChangesFromChordFile(file))
return changes
})
break
}
case "layout": {
changes.update(changes => {
changes.push(...getChangesFromLayoutFile(file))
return changes
})
break
}
case "settings": {
changes.update(changes => {
changes.push(...getChangesFromSettingsFile(file))
return changes
})
break
}
default: {
throw new Error(`Unknown backup type "${(file as CharaFile<string>).type}"`)
}
}
}
export function getChangesFromChordFile(file: CharaChordFile) {
const changes: Change[] = []
const existingChords = new Set(get(chords).map(({phrase, actions}) => JSON.stringify([actions, phrase])))
for (const [input, output] of file.chords) {
if (existingChords.has(JSON.stringify([input, output]))) {
continue
}
changes.push({
type: ChangeType.Chord,
actions: input,
phrase: output,
id: input,
})
}
return changes
}
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
const changes: Change[] = []
for (const [id, value] of file.settings.entries()) {
const setting = get(settings)[id]
if (setting !== undefined && setting.value !== value) {
changes.push({
type: ChangeType.Setting,
id,
setting: value,
})
}
}
return changes
}
export function getChangesFromLayoutFile(file: CharaLayoutFile) {
const changes: Change[] = []
for (const [layer, keys] of file.layout.entries()) {
for (const [id, action] of keys.entries()) {
if (get(layout)[layer][id].action !== action) {
changes.push({
type: ChangeType.Layout,
layer,
id,
action,
})
}
}
}
return changes
}

View 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
1 e + b + a babe
2 e + c + b because
3 f + e + c + a face
4 h + e + c + a each
5 i + d + ' I'd
6 i + g + b big
7 i + g + e give
8 k + b + a back
9 k + e + a take
10 l + e + a late
11 l + e + d + a lead
12 l + f + e feel
13 l + g + e + a large
14 l + h + e help
15 l + i + a Lia
16 l + i + f fill
17 l + i + f + e life
18 l + i + g + b + a gitlab
19 l + k + i + e like
20 m + e + a make
21 m + i + ' I'm
22 n + c + a can
23 n + d + a and
24 n + e + b been
25 n + e + b + a enable
26 n + e + d end

View 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

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

View File

@@ -0,0 +1,24 @@
{
"charaVersion": 1,
"type": "layout",
"device": "one",
"layout": [
[
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262, 288, 277, 298, 307, 264, 287,
268, 332, 311, 274, 286, 308, 329, 310, 280, 358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315,
314, 317, 316, 312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263, 293, 260, 296,
544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278, 357, 516, 519, 517, 518, 327, 336, 338, 335,
337, 328, 325, 322, 323, 324
],
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
]
}

View 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
1 A1 0 309
2 A1 1 304
3 A1 2 312
4 A1 3 303
5 A1 4 306
6 A1 5 290
7 A1 6 282
8 A1 7 301
9 A1 8 266
10 A1 9 285
11 A1 10 289
12 A1 11 270
13 A1 12 281
14 A1 13 272
15 A1 14 262
16 A1 15 288
17 A1 16 277
18 A1 17 298
19 A1 18 307
20 A1 19 264
21 A1 20 287
22 A1 21 268
23 A1 22 332
24 A1 23 311
25 A1 24 274
26 A1 25 286
27 A1 26 308
28 A1 27 329
29 A1 28 310
30 A1 29 280
31 A1 30 358
32 A1 31 512
33 A1 32 515
34 A1 33 513
35 A1 34 514
36 A1 35 313
37 A1 36 319
38 A1 37 318
39 A1 38 321
40 A1 39 320
41 A1 40 326
42 A1 41 315
43 A1 42 314
44 A1 43 317
45 A1 44 316
46 A1 45 312
47 A1 46 330
48 A1 47 331
49 A1 48 333
50 A1 49 334
51 A1 50 291
52 A1 51 261
53 A1 52 283
54 A1 53 536
55 A1 54 276
56 A1 55 292
57 A1 56 265
58 A1 57 275
59 A1 58 267
60 A1 59 263
61 A1 60 293
62 A1 61 260
63 A1 62 296
64 A1 63 544
65 A1 64 279
66 A1 65 294
67 A1 66 271
68 A1 67 299
69 A1 68 269
70 A1 69 273
71 A1 70 295
72 A1 71 284
73 A1 72 297
74 A1 73 302
75 A1 74 278
76 A1 75 357
77 A1 76 516
78 A1 77 519
79 A1 78 517
80 A1 79 518
81 A1 80 327
82 A1 81 336
83 A1 82 338
84 A1 83 335
85 A1 84 337
86 A1 85 328
87 A1 86 325
88 A1 87 322
89 A1 88 323
90 A1 89 324
91 A2 0 0
92 A2 1 0
93 A2 2 0
94 A2 3 0
95 A2 4 0
96 A2 5 0
97 A2 6 0
98 A2 7 0
99 A2 8 0
100 A2 9 0
101 A2 10 0
102 A2 11 0
103 A2 12 0
104 A2 13 0
105 A2 14 0
106 A2 15 0
107 A2 16 0
108 A2 17 0
109 A2 18 0
110 A2 19 0
111 A2 20 0
112 A2 21 0
113 A2 22 0
114 A2 23 0
115 A2 24 0
116 A2 25 0
117 A2 26 0
118 A2 27 0
119 A2 28 0
120 A2 29 0
121 A2 30 0
122 A2 31 0
123 A2 32 0
124 A2 33 0
125 A2 34 0
126 A2 35 0
127 A2 36 0
128 A2 37 0
129 A2 38 0
130 A2 39 0
131 A2 40 0
132 A2 41 0
133 A2 42 0
134 A2 43 0
135 A2 44 0
136 A2 45 0
137 A2 46 0
138 A2 47 0
139 A2 48 0
140 A2 49 0
141 A2 50 0
142 A2 51 0
143 A2 52 0
144 A2 53 0
145 A2 54 0
146 A2 55 0
147 A2 56 0
148 A2 57 0
149 A2 58 0
150 A2 59 0
151 A2 60 0
152 A2 61 0
153 A2 62 0
154 A2 63 0
155 A2 64 0
156 A2 65 0
157 A2 66 0
158 A2 67 0
159 A2 68 0
160 A2 69 0
161 A2 70 0
162 A2 71 0
163 A2 72 0
164 A2 73 0
165 A2 74 0
166 A2 75 0
167 A2 76 0
168 A2 77 0
169 A2 78 0
170 A2 79 0
171 A2 80 0
172 A2 81 0
173 A2 82 0
174 A2 83 0
175 A2 84 0
176 A2 85 0
177 A2 86 0
178 A2 87 0
179 A2 88 0
180 A2 89 0
181 A3 0 0
182 A3 1 0
183 A3 2 0
184 A3 3 0
185 A3 4 0
186 A3 5 0
187 A3 6 0
188 A3 7 0
189 A3 8 0
190 A3 9 0
191 A3 10 0
192 A3 11 0
193 A3 12 0
194 A3 13 0
195 A3 14 0
196 A3 15 0
197 A3 16 0
198 A3 17 0
199 A3 18 0
200 A3 19 0
201 A3 20 0
202 A3 21 0
203 A3 22 0
204 A3 23 0
205 A3 24 0
206 A3 25 0
207 A3 26 0
208 A3 27 0
209 A3 28 0
210 A3 29 0
211 A3 30 0
212 A3 31 0
213 A3 32 0
214 A3 33 0
215 A3 34 0
216 A3 35 0
217 A3 36 0
218 A3 37 0
219 A3 38 0
220 A3 39 0
221 A3 40 0
222 A3 41 0
223 A3 42 0
224 A3 43 0
225 A3 44 0
226 A3 45 0
227 A3 46 0
228 A3 47 0
229 A3 48 0
230 A3 49 0
231 A3 50 0
232 A3 51 0
233 A3 52 0
234 A3 53 0
235 A3 54 0
236 A3 55 0
237 A3 56 0
238 A3 57 0
239 A3 58 0
240 A3 59 0
241 A3 60 0
242 A3 61 0
243 A3 62 0
244 A3 63 0
245 A3 64 0
246 A3 65 0
247 A3 66 0
248 A3 67 0
249 A3 68 0
250 A3 69 0
251 A3 70 0
252 A3 71 0
253 A3 72 0
254 A3 73 0
255 A3 74 0
256 A3 75 0
257 A3 76 0
258 A3 77 0
259 A3 78 0
260 A3 79 0
261 A3 80 0
262 A3 81 0
263 A3 82 0
264 A3 83 0
265 A3 84 0
266 A3 85 0
267 A3 86 0
268 A3 87 0
269 A3 88 0
270 A3 89 0

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

View File

@@ -0,0 +1,25 @@
import type {CharaLayoutFile} from "$lib/share/chara-file"
/**
* Converts a legacy CSV-based layout to the modern JSON-based format
*/
export function csvLayoutToJson(csv: string, device: CharaLayoutFile["device"] = "one"): CharaLayoutFile {
const layout: CharaLayoutFile = {
charaVersion: 1,
type: "layout",
device,
layout: [[], [], []],
}
for (const layer of csv.trim().split("\n")) {
const [layerId, key, action] = layer.substring(1).split(",").map(Number)
layout.layout[Number(layerId) - 1][Number(key)] = Number(action)
}
return layout
}
export function isCsvLayout(csv: string): boolean {
return /^(A[123],\d+,\d+\n?)+$/.test(csv)
}

View File

@@ -1,23 +0,0 @@
import type {Chord} from "$lib/serial/chord"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
interface Language {
name: string
noLazyMode?: boolean
orderedByFrequency?: boolean
words: string[]
}
export async function calculateChordCoverage(chords: Chord[]) {
const language: Language = await fetch("/languages/english.json").then(it => it.json())
const words = new Set(language.words)
for (const chord of chords) {
words.delete(chord.phrase.map(it => KEYMAP_CODES[it].id!).join(""))
}
return {
coverage: words.size / language.words.length,
missing: [...words.values()],
}
}

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import type {KeyInfo} from "$lib/serial/keymap-codes"
import {action as title} from "$lib/title"
import {osLayout} from "$lib/os-layout"
import LL from "../../i18n/i18n-svelte"
export let action: number | KeyInfo
export let display: "inline-keys" | "keys" = "inline-keys"
$: info = typeof action === "number" ? KEYMAP_CODES[action] ?? {code: action} : action
$: dynamicMapping = info.keyCode && $osLayout.get(info.keyCode)
$: tooltip =
(info.title ?? info.id ?? `0x${info.code.toString(16)}`) +
(info.variant === "left" ? " (left)" : info.variant === "right" ? " (right)" : "")
</script>
{#if dynamicMapping}
<span
use:title={{title: $LL.actionSearch.LIVE_LAYOUT_INFO()}}
class="dynamic"
class:left={info.variant === "left"}
class:right={info.variant === "right"}
class:inline={display === "inline-keys"}>{dynamicMapping}</span
>
{:else if display === "keys"}
<kbd
class:icon={!!info.icon}
class:left={info.variant === "left"}
class:right={info.variant === "right"}
use:title={{title: tooltip}}
>
{info.icon ?? info.display ?? info.id ?? `0x${info.code.toString(16)}`}
</kbd>
{:else if display === "inline-keys"}
{#if !info.icon && info.id?.length === 1}
<span class:left={info.variant === "left"} class:right={info.variant === "right"}>{info.id}</span>
{:else}
<kbd
class="inline-kbd"
class:left={info.variant === "left"}
class:right={info.variant === "right"}
class:icon={!!info.icon}
use:title={{title: tooltip}}
>
{info.icon ?? info.display ?? info.id ?? `0x${info.code.toString(16)}`}</kbd
>
{/if}
{/if}
<style lang="scss">
kbd:not(.inline-kbd) {
height: 24px;
padding-block: auto;
transition: color 250ms ease;
}
.left {
border-left-width: 3px;
}
.right {
border-right-width: 3px;
}
.dynamic {
padding: 4px;
border-radius: 1px;
min-width: 8px;
background: var(--md-sys-color-surface-variant);
&.inline {
padding: 0px;
}
}
.inline-kbd {
margin-inline-end: 2px;
}
:global(span) + .inline-kbd {
margin-inline-start: 2px;
}
</style>

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import ActionListItem from "$lib/components/ActionListItem.svelte"
export let exact: number | undefined = undefined
export let code: number = Number.NaN
export let results: number[] = []
export let width: number
</script>
<div class="list" style="width: {width}px">
{#if exact !== undefined}
<div class="exact">
<i>Exact match</i>
<ActionListItem id={exact} />
</div>
{/if}
{#if !exact && code}
{#if code >= 2 ** 5 && code < 2 ** 13}
<button>USE CODE</button>
{:else}
<div>Action code is out of range</div>
{/if}
{/if}
{#each results as id (id)}
<ActionListItem {id} />
{/each}
</div>
<style lang="scss">
.list {
--scrollbar-color: var(--md-sys-color-on-surface-variant);
scrollbar-gutter: stable both-edges;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
max-height: 500px;
padding-block: 8px;
}
.exact {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
border: 1px solid var(--md-sys-color-primary);
border-radius: 8px;
> i {
padding-inline: 8px;
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
border-radius: 0 0 8px 8px;
}
}
</style>

View File

@@ -1,13 +1,15 @@
<script lang="ts">
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import type {KeyInfo} from "$lib/serial/keymap-codes"
import LL from "../../i18n/i18n-svelte"
import Action from "$lib/components/Action.svelte"
export let id: number | KeyInfo
$: key = (typeof id === "number" ? KEYMAP_CODES[id] ?? id : id) as number | KeyInfo
</script>
<button>
<button on:click>
{#if typeof key === "object"}
<div class="title">
<b>
@@ -21,8 +23,14 @@
{#if key.description}
<i>{key.description}</i>
{/if}
{#if key.category.name === "ASCII Macros"}
<span class="warning">{@html $LL.actionSearch.SHIFT_WARNING()}</span>
{/if}
{#if key.category.name === "CP-1252"}
<span class="warning">{@html $LL.actionSearch.ALT_CODE_WARNING()}</span>
{/if}
</div>
<span class:icon={!!key.icon} class="key">{key.icon || key.id || `0x${key.code.toString(16)}`}</span>
<Action display="keys" action={key} />
{:else}
<span class="key">0x{key.toString(16)}</span>
{/if}
@@ -35,6 +43,7 @@
align-items: center;
width: 100%;
height: auto;
margin: 0;
padding: 8px;
@@ -43,6 +52,13 @@
background: transparent;
border: none;
border-radius: 8px;
&:focus-visible {
color: var(--md-sys-color-on-surface-variant);
background: var(--md-sys-color-surface-variant);
outline: none;
}
}
.title {
@@ -55,17 +71,14 @@
text-align: start;
}
.key {
.warning {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--md-sys-color-error);
min-width: 32px;
padding: 4px;
font-weight: 600;
border: 1px solid currentcolor;
border-radius: 4px;
> :global(.icon) {
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import Action from "$lib/components/Action.svelte"
import type {KeyInfo} from "$lib/serial/keymap-codes"
export let actions: Array<number | KeyInfo>
export let display: "keys" | "inline-keys" = "inline-keys"
</script>
{#each actions as action, i (`${typeof action === "number" ? action : action.code}:${i}`)}
<Action {action} {display} />
{/each}

View File

@@ -5,7 +5,9 @@
</script>
{#if $needRefresh}
<button title="Update ready" class="icon" on:click={() => updateServiceWorker(true)}>update</button>
<button title="Update ready" on:click={() => updateServiceWorker(true)}
>Update <span class="icon">update</span></button
>
{:else if $offlineReady}
<div title="App can now be used offline" class="icon">offline_pin</div>
{/if}

View File

@@ -2,7 +2,7 @@
import {serialLog, serialPort} from "$lib/serial/connection"
import {slide} from "svelte/transition"
function submit(event: InputEvent) {
function submit(event: Event) {
event.preventDefault()
$serialPort.send(value.trim())
value = ""
@@ -16,7 +16,13 @@
<form on:submit={submit}>
<div bind:this={io} class="io">
{#each $serialLog as { type, value }}
<p class={type} transition:slide>{value}</p>
{#if type === "input"}
<code transition:slide>{value}</code>
{:else if type === "output"}
<samp transition:slide>{value}</samp>
{:else}
<p transition:slide>{value}</p>
{/if}
{/each}
<div class="anchor" />
</div>
@@ -111,17 +117,15 @@
height: 1px;
}
code,
samp,
p {
display: block;
overflow-anchor: none;
margin-block: 0.15rem;
}
p.input {
margin-block-end: 0.25rem;
font-weight: bold;
}
p.system {
p {
display: flex;
justify-content: center;
@@ -134,8 +138,9 @@
border-radius: 8px;
}
p.input::before {
code::before {
content: "> ";
margin-block-end: 0.25rem;
font-weight: 900;
color: var(--md-sys-color-primary);
}

View File

@@ -0,0 +1,22 @@
<script lang="ts">
export let title: string | undefined
export let shortcut: string | undefined
</script>
{#if title}
<p>{@html title}</p>
{/if}
{#if shortcut}
<kbd>
{#each shortcut.split("+") as key}
<kbd>{key}</kbd>
{/each}
</kbd>
{/if}
<style lang="scss">
p {
margin-block: 0;
}
</style>

View File

@@ -1,77 +0,0 @@
<script lang="ts">
import LayoutCC1 from "$lib/components/layout/LayoutCC1.svelte"
import {chords, highlightActions} from "$lib/serial/connection"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
$: content = Array.from({length: 10}).map(() => $chords[Math.floor(Math.random() * $chords.length)])
let cursor = [0, 0]
let input = []
$: {
$highlightActions = content[cursor[0]]?.actions ?? []
}
function keypress(event: KeyboardEvent) {
cursor++
input.push(event.key)
}
</script>
<svelte:window on:keypress={keypress} />
<div>
<section>
<!-- <div class="cursor" style="translate: calc({cursor}ch - 50%) -50%" /> -->
{#each content as word, i}
{#if word}
{#each word.phrase as letter, j}
<span>{KEYMAP_CODES[letter].id}</span>
{/each}
&nbsp;
{/if}
{/each}
</section>
<LayoutCC1 />
</div>
<style lang="scss">
div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
height: 100%;
}
section {
position: relative;
display: flex;
flex-direction: row;
font-size: 1.3rem;
font-weight: 500;
}
.letter {
position: relative;
filter: brightness(50%);
}
.cursor {
position: absolute;
top: 50%;
left: 0;
translate: -50% -50%;
width: 2px;
height: 1em;
background: var(--md-sys-color-primary);
transition: all 250ms ease;
}
</style>

View File

@@ -1,167 +1,302 @@
<script lang="ts">
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
import charaActions from "$lib/assets/keymaps/chara-chorder.yml"
import mouseActions from "$lib/assets/keymaps/mouse.yml"
import keyboardActions from "$lib/assets/keymaps/keyboard.yml"
import asciiActions from "$lib/assets/keymaps/ascii.yml"
import cp1252Actions from "$lib/assets/keymaps/cp-1252.yml"
import FlexSearch from "flexsearch"
import {KEYMAP_CATEGORIES, KEYMAP_CODES} from "$lib/serial/keymap-codes"
import type {KeyInfo} from "$lib/serial/keymap-codes"
import Index from "flexsearch"
import {createEventDispatcher} from "svelte"
import ActionListItem from "$lib/components/ActionListItem.svelte"
import LL from "../../../i18n/i18n-svelte"
import {action} from "$lib/title"
const index = new FlexSearch({tokenize: "full"})
export let currentAction: number | undefined = undefined
export let nextAction: number | undefined = undefined
for (const code in KEYMAP_CODES) {
const key = KEYMAP_CODES[code]
index.add(
code,
`${key.id || key.code} ${key.title || ""} ${key.variant || ""} ${key.description || ""}`.trim(),
const index = new Index({tokenize: "full"})
for (const action of Object.values(KEYMAP_CODES)) {
index?.add(
action.code,
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
action.description || ""
}`,
)
}
const exactIndex: Record<string, KeyInfo> = Object.fromEntries(
Object.values(KEYMAP_CODES)
.filter(it => !!it.id)
.map(it => [it.id, it] as const),
)
function search() {
const query = searchInput.value
customValue = query && !Number.isNaN(Number(query)) ? Number(query) : undefined
results = query ? index.search(searchInput.value) : defaultActions
results = index!.search(searchBox.value)
exact = exactIndex[searchBox.value]?.code
code = Number(searchBox.value)
}
let customValue = undefined
const defaultActions: string[] = [
charaActions,
mouseActions,
keyboardActions,
asciiActions,
cp1252Actions,
].flatMap(it => Object.keys(it.actions))
let results: string[] = defaultActions
let searchInput: HTMLInputElement
function select(id?: number) {
if (id !== undefined) {
dispatch("select", id)
}
}
function keyboardNavigation(event: KeyboardEvent) {
if (event.shiftKey && event.key === "Enter") {
dispatch("select", exact)
} else if (event.key === "ArrowDown") {
const element =
resultList.querySelector("li:focus-within")?.nextSibling ?? resultList.querySelector("li:not(.exact)")
if (element instanceof HTMLLIElement) {
element.querySelector("button")?.focus()
}
} else if (event.key === "ArrowUp") {
const element =
resultList.querySelector("li:focus-within")?.previousSibling ??
resultList.querySelector("li:not(.exact)")
if (element instanceof HTMLLIElement) {
element.querySelector("button")?.focus()
}
} else {
searchBox.focus()
return
}
event.preventDefault()
}
let results: number[] = Object.keys(KEYMAP_CODES).map(Number)
let exact: number | undefined = undefined
let code: number = Number.NaN
const dispatch = createEventDispatcher()
let searchBox: HTMLInputElement
let resultList: HTMLUListElement
let filter: Set<number>
</script>
<section>
<input type="search" on:input={search} placeholder="Search Actions" bind:this={searchInput} />
<svelte:window on:keydown={keyboardNavigation} />
<div class="results">
{#if customValue !== undefined}
<button class="custom">
Custom ActionID
<span class="key">0x{customValue.toString(16).toUpperCase()}</span>
</button>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<dialog open on:click|self={() => dispatch("close")}>
<div class="content">
<div class="search-row">
<input
type="search"
bind:this={searchBox}
on:input={search}
on:keypress={event => {
if (event.key === "Enter") {
select(exact)
}
}}
placeholder={$LL.actionSearch.PLACEHOLDER()}
/>
<button on:click={() => select(0)} use:action={{shortcut: "shift+esc"}}
>{$LL.actionSearch.DELETE()}</button
>
<button
use:action={{title: $LL.modal.CLOSE(), shortcut: "esc"}}
class="icon"
on:click={() => dispatch("close")}>close</button
>
</div>
<fieldset class="filters">
<label
>{$LL.actionSearch.filter.ALL()}<input
checked
name="category"
type="radio"
value={undefined}
bind:group={filter}
/></label
>
{#each KEYMAP_CATEGORIES as category}
<label
>{category.name}<input
name="category"
type="radio"
value={new Set(Object.keys(category.actions).map(Number))}
bind:group={filter}
/></label
>
{/each}
</fieldset>
{#if currentAction !== undefined}
<aside>
<h3>{$LL.actionSearch.CURRENT_ACTION()}</h3>
<ActionListItem id={currentAction} />
</aside>
{#if nextAction}
<aside>
<h3>{$LL.actionSearch.NEXT_ACTION()}</h3>
<ActionListItem id={nextAction} />
</aside>
{/if}
{/if}
{#each results as id}
{@const key = KEYMAP_CODES[id]}
<button title={key.description}>
<div class="title">
<b>
{key.title || ""}
{#if key.variant === "left"}
(Left)
{:else if key.variant === "right"}
(Right)
{/if}
</b>
{#if key.description}
<i>{key.description}</i>
{/if}
</div>
<span class:icon={!!key.icon} class="key">{key.icon || key.id || key.code}</span>
</button>
{/each}
<ul bind:this={resultList}>
{#if exact !== undefined}
<li class="exact">
<i>Exact match</i>
<ActionListItem id={exact} on:click={() => select(exact)} />
</li>
{/if}
{#if !exact && code}
{#if code >= 2 ** 5 && code < 2 ** 13}
<li><button on:click={() => select(code)}>USE CODE</button></li>
{:else}
<li>Action code is out of range</li>
{/if}
{/if}
{#each filter ? results.filter(it => filter.has(it)) : results as id (id)}
<li><ActionListItem {id} on:click={() => select(id)} /></li>
{/each}
</ul>
</div>
</section>
</dialog>
<style lang="scss">
section {
.filters {
display: flex;
flex-direction: column;
gap: 8px;
width: calc(min(100vw - 10px, 512px));
height: calc(min(90vh, 600px));
}
input[type="search"] {
width: 100%;
height: 48px;
padding-inline: 16px;
font-family: "Noto Sans Mono", monospace;
font-size: 18px;
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
gap: 4px;
border: none;
border-radius: 24px;
&::placeholder {
color: inherit;
opacity: 0.3;
}
label {
height: unset;
padding-block: 2px;
padding-inline: 4px;
&::after {
content: "plus";
font-size: 14px;
border: 1px solid currentcolor;
border-radius: 6px;
&:has(:checked) {
color: var(--md-sys-color-on-secondary);
background: var(--md-sys-color-secondary);
}
input {
display: none;
}
}
}
.key {
overflow: hidden;
dialog {
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
padding: 4px;
font-size: 18px;
text-overflow: ellipsis;
border: 1px solid var(--md-sys-color-outline);
border-radius: 6px;
}
.title {
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-start;
text-align: start;
> b {
font-size: 18px;
}
}
button {
cursor: pointer;
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
width: 100%;
height: 100%;
font-family: "Noto Sans Mono", monospace;
font-size: 14px;
color: inherit;
background: transparent;
background: rgba(0 0 0 / 60%);
border: none;
}
.custom {
padding: 8px;
padding-inline-start: 16px;
aside {
pointer-events: none;
margin: 8px;
opacity: 0.4;
border: 1px dashed var(--md-sys-color-outline);
border-radius: 8px;
> h3 {
width: fit-content;
margin-block-start: -13px;
margin-block-end: 0;
margin-inline-start: 16px;
padding-inline: 8px;
background: var(--md-sys-color-background);
}
}
.search-row {
display: flex;
gap: 4px;
align-items: center;
margin-inline: 16px;
}
.content {
position: relative;
transform-origin: top left;
overflow: hidden;
display: flex;
flex-direction: column;
width: calc(min(30cm, 90%));
height: calc(min(100% - 128px, 90%));
color: var(--md-sys-color-on-background);
background: var(--md-sys-color-background);
border-radius: 16px;
}
.results {
overflow-y: scroll;
input[type="search"] {
width: 100%;
height: 64px;
margin-block-end: 8px;
padding-inline: 16px;
font-family: inherit;
font-size: 16px;
color: currentcolor;
background: none;
border: none;
border-bottom: 1px solid var(--md-sys-color-surface-variant);
transition: all 250ms ease;
&:focus {
border-bottom: 1px solid var(--md-sys-color-primary);
outline: none;
}
}
ul {
--scrollbar-color: var(--md-sys-color-surface-variant);
scrollbar-gutter: both-edges stable;
overflow-y: auto;
box-sizing: border-box;
height: 100%;
margin: 0;
padding: 0;
padding-inline: 4px;
}
li {
display: contents;
}
.exact {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
margin-block-start: 8px;
border: 1px solid var(--md-sys-color-primary);
border-radius: 8px;
> i {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
padding-inline: 6px;
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
border-radius: 0 0 8px 8px;
}
}
</style>

View File

@@ -0,0 +1,206 @@
<script lang="ts">
import {compileLayout} from "$lib/serialization/visual-layout"
import type {VisualLayout, CompiledLayoutKey} from "$lib/serialization/visual-layout"
import {deviceLayout} from "$lib/serial/connection"
import {dev} from "$app/environment"
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
import {get} from "svelte/store"
import type {Writable} from "svelte/store"
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte"
import {getContext} from "svelte"
import type {VisualLayoutConfig} from "./visual-layout.js"
import {changes, ChangeType, layout} from "$lib/undo-redo"
const {scale, margin, strokeWidth, fontSize, iconFontSize} =
getContext<VisualLayoutConfig>("visual-layout-config")
const activeLayer = getContext<Writable<number>>("active-layer")
if (dev) {
// you have absolutely no idea what a difference this makes for performance
console.assert(scale % 1 === 0, "Scale must be an integer")
console.assert((scale / 2) % 1 === 0, "Scale must be divisible by 2")
console.assert(strokeWidth % 1 === 0, "Stroke must be an integer")
console.assert(margin % 1 === 0, "Margin must be an integer")
console.assert(fontSize % 1 === 0, "Font size must be an integer")
console.assert(iconFontSize % 1 === 0, "Icon font size must be an integer")
}
export let visualLayout: VisualLayout
$: layoutInfo = compileLayout(visualLayout)
function getCenter(key: CompiledLayoutKey): [x: number, y: number] {
return [key.pos[0] + key.size[0] / 2, key.pos[1] + key.size[1] / 2]
}
function getDistance(a: CompiledLayoutKey, b: CompiledLayoutKey) {
const x1 = a.pos[0] + margin
const y1 = a.pos[1] + margin
const x1b = x1 + a.size[0] - margin
const y1b = y1 + a.size[1] - margin
const x2 = b.pos[0] + margin
const y2 = b.pos[1] + margin
const x2b = x2 + b.size[0] - margin
const y2b = y2 + b.size[1] - margin
const left = x2b < x1
const right = x1b < x2
const bottom = y2b < y1
const top = y1b < y2
return top && left
? Math.sqrt((x1 - x2b) ** 2 + (y1b - y2) ** 2)
: left && bottom
? Math.sqrt((x1 - x2b) ** 2 + (y1 - y2b) ** 2)
: bottom && right
? Math.sqrt((x1b - x2) ** 2 + (y1 - y2b) ** 2)
: right && top
? Math.sqrt((x1b - x2) ** 2 + (y1b - y2) ** 2)
: left
? x1 - x2b
: right
? x2 - x1b
: bottom
? y1 - y2b
: top
? y2 - y1b
: 0
}
function navigate(event: KeyboardEvent) {
if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) return
let wantedAngle: number
const angleThreshold = Math.PI
if (event.key === "ArrowUp") wantedAngle = Math.PI
else if (event.key === "ArrowDown") wantedAngle = 0
else if (event.key === "ArrowRight") wantedAngle = Math.PI / 2
else if (event.key === "ArrowLeft") wantedAngle = -Math.PI / 2
else return
event.preventDefault()
if (!focusKey) (groupParent.firstChild as SVGGElement).focus()
const [focusX, focusY] = getCenter(focusKey)
let bestDistance = Infinity
let bestCandidate = 0
let isOptimalAngle = false
for (const [i, key] of layoutInfo.keys.entries()) {
if (key === focusKey) continue
const [keyX, keyY] = getCenter(key)
const deltaX = keyX - focusX
const deltaY = keyY - focusY
const angle = Math.atan2(deltaX, deltaY)
const distance = getDistance(key, focusKey)
const angleDelta = Math.abs(wantedAngle - angle)
if (isOptimalAngle ? angleDelta > Number.EPSILON : angleDelta >= angleThreshold) continue
if (distance > bestDistance) continue
bestDistance = distance
bestCandidate = i
isOptimalAngle = angleDelta <= Number.EPSILON
}
const node = groupParent.children.item(bestCandidate)
if (node instanceof SVGGElement) {
node.focus()
}
}
function edit(index: number) {
const keyInfo = layoutInfo.keys[index]
const clickedGroup = groupParent.children.item(index) as SVGGElement
const nextAction = get(layout)[get(activeLayer)][keyInfo.id]
const currentAction = get(deviceLayout)[get(activeLayer)][keyInfo.id]
const component = new ActionSelector({
target: document.body,
props: {
currentAction,
nextAction: nextAction.isApplied ? undefined : nextAction.action,
},
})
const dialog = document.querySelector("dialog > div") as HTMLDivElement
const backdrop = document.querySelector("dialog") as HTMLDialogElement
const dialogRect = dialog.getBoundingClientRect()
const groupRect = clickedGroup.getBoundingClientRect()
const scale = 0.5
const dialogScale = `${1 - scale * (1 - groupRect.width / dialogRect.width)} ${
1 - scale * (1 - groupRect.height / dialogRect.height)
}`
const dialogTranslate = `${scale * (groupRect.x - dialogRect.x)}px ${
scale * (groupRect.y - dialogRect.y)
}px`
const duration = 150
const options = {duration, easing: "ease"}
const dialogAnimation = dialog.animate(
[
{scale: dialogScale, translate: dialogTranslate},
{translate: "0 0", scale: "1"},
],
options,
)
const backdropAnimation = backdrop.animate([{opacity: 0}, {opacity: 1}], options)
async function closed() {
dialogAnimation.reverse()
backdropAnimation.reverse()
await dialogAnimation.finished
component.$destroy()
}
component.$on("close", closed)
component.$on("select", ({detail}) => {
changes.update(changes => {
changes.push({
type: ChangeType.Layout,
id: keyInfo.id,
layer: get(activeLayer),
action: detail,
})
return changes
})
closed()
})
}
let focusKey: CompiledLayoutKey
let groupParent: SVGElement
</script>
<svelte:window on:keydown={navigate} />
<svg
class="print"
viewBox="0 0 {layoutInfo.size[0] * scale} {layoutInfo.size[1] * scale}"
bind:this={groupParent}
>
{#each layoutInfo.keys as key, i}
<KeyboardKey
{i}
{key}
on:focusin={() => (focusKey = key)}
on:click={() => edit(i)}
on:keypress={({key}) => {
if (key === "Enter") {
edit(i)
}
}}
/>
{/each}
</svg>
<style lang="scss">
svg {
overflow: visible;
grid-area: "d";
width: calc(min(100%, 35cm));
max-height: calc(100% - 170px);
}
</style>

View File

@@ -1,18 +0,0 @@
<script lang="ts">
import {layout} from "$lib/serial/connection"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
import {popup} from "$lib/popup"
import ActionListItem from "$lib/components/ActionListItem.svelte"
export let id: number = 0
</script>
<table>
{#each $layout as layer, i}
<tr>
<th class="icon">counter_{i + 1}</th>
<ActionListItem id={layer[id]} />
</tr>
{/each}
</table>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import {getContext} from "svelte"
import type {Writable} from "svelte/store"
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
import type {CompiledLayoutKey} from "$lib/serialization/visual-layout"
import {layout} from "$lib/undo-redo.js"
import {osLayout} from "$lib/os-layout.js"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import {action} from "$lib/title"
const {fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize} =
getContext<VisualLayoutConfig>("visual-layout-config")
const activeLayer = getContext<Writable<number>>("active-layer")
export let key: CompiledLayoutKey
export let fontSizeMultiplier = 1
export let middle: [number, number]
export let pos: [number, number]
export let rotate: number
export let positions: [[number, number], [number, number], [number, number]]
</script>
{#each positions as position, layer}
{@const {action: actionId, isApplied} = $layout[layer][key.id] ?? {action: 0, isApplied: true}}
{@const {code, icon, id, display, title, keyCode, variant} = KEYMAP_CODES[actionId] ?? {code: actionId}}
{@const dynamicMapping = keyCode && $osLayout.get(keyCode)}
{@const tooltip =
(title ?? id ?? `0x${code.toString(16)}`) +
(variant === "left" ? " (left)" : variant === "right" ? " (right)" : "")}
{@const isActive = layer === $activeLayer}
{@const direction = [
Math.sign(middle[0]) * (Math.abs(middle[0]) - margin * 3) * position[0],
Math.sign(middle[1]) * (Math.abs(middle[1]) - margin * 3) * position[1],
]}
{@const hasIcon = !dynamicMapping && !!icon}
<text
fill={isApplied ? "currentcolor" : "var(--md-sys-color-primary)"}
font-weight={isApplied ? "" : "bold"}
text-anchor="middle"
alignment-baseline="central"
x={pos[0] + middle[0] + (isApplied ? 0 : fontSize / 3)}
y={pos[1] + middle[1]}
font-size={fontSizeMultiplier * (hasIcon ? iconFontSize : fontSize)}
font-family={hasIcon ? "Material Symbols Rounded" : undefined}
opacity={isActive ? 1 : inactiveOpacity}
style:scale={isActive ? 1 : inactiveScale}
style:translate={isActive
? "0 0 0"
: `${direction[0].toPrecision(2)}px ${direction[1].toPrecision(2)}px 0`}
style:rotate="{rotate}deg"
use:action={{title: tooltip}}
>
{#if code !== 0}
{dynamicMapping || icon || display || id || `0x${code.toString(16)}`}
{/if}
{#if !isApplied}
<tspan></tspan>
{/if}
</text>
{/each}
<style lang="scss">
$focus-transition: 10ms;
$transition: 200ms;
text {
will-change: translate, scale;
user-select: none;
transform-origin: center;
transform-box: fill-box;
transition:
fill #{$focus-transition} ease,
opacity #{$transition} ease,
translate #{$transition} ease,
scale #{$transition} ease;
}
text:focus-within {
outline: none;
}
</style>

View File

@@ -0,0 +1,116 @@
<script lang="ts">
import type {CompiledLayoutKey} from "$lib/serialization/visual-layout"
import {getContext} from "svelte"
import type {VisualLayoutConfig} from "./visual-layout.js"
import KeyText from "$lib/components/layout/KeyText.svelte"
const {scale, margin, strokeWidth} = getContext<VisualLayoutConfig>("visual-layout-config")
export let i: number
export let key: CompiledLayoutKey
$: posX = key.pos[0] * scale
$: posY = key.pos[1] * scale
$: sizeX = key.size[0] * scale
$: sizeY = key.size[1] * scale
</script>
<g class="key-group" on:click on:keypress on:focusin role="button" tabindex={i + 1}>
{#if key.shape === "square"}
<rect
x={posX + margin}
y={posY + margin}
rx={key.cornerRadius * scale}
width={sizeX - margin * 2}
height={sizeY - margin * 2}
stroke-width={strokeWidth}
/>
<KeyText
{key}
middle={[sizeX / 2, sizeY / 2]}
pos={[posX, posY]}
rotate={-key.rotate}
positions={[
[-1, 1],
[-1, -1],
[1, -1],
]}
/>
{:else if key.shape === "quarter-circle"}
{@const innerMargin = margin / 2}
{@const r1 = sizeX / 2 - margin}
{@const p1 = r1 - innerMargin}
{@const r2 = r1 - sizeY + innerMargin * 2}
{@const p2 = r2 - innerMargin}
{@const multiplier = 1.25}
{@const rotateRad = (key.rotate + 45) * (Math.PI / 180)}
{@const rotX = Math.round((Math.abs(Math.cos(rotateRad - Math.PI / 2)) + Number.EPSILON) * 100) / 100}
{@const rotY = Math.round((Math.abs(Math.sin(rotateRad - Math.PI / 2)) + Number.EPSILON) * 100) / 100}
{@const rc = r1 - (r1 - r2) / 2}
{@const middleX = Math.cos(rotateRad) * rc}
{@const middleY = Math.sin(rotateRad) * rc}
<path
style:transform="rotateZ({key.rotate}deg) translate({innerMargin}px, {innerMargin}px)"
d="M{posX + p1},{posY} a{r1},{r1} 0 0,1 {-p1},{p1} l0,{-(p1 - p2)} a{r2},{r2} 0 0,0 {p2},{-p2}z"
/>
<KeyText
{key}
middle={[middleX, middleY]}
pos={[posX, posY]}
rotate={0}
fontSizeMultiplier={multiplier}
positions={[
[-rotY, -rotX],
[-rotX, -rotY],
[rotX, rotY],
]}
/>
{/if}
</g>
<style lang="scss">
$focus-transition: 10ms;
$transition: 200ms;
rect {
transform-origin: center;
transform-box: fill-box;
}
path,
g {
transform-origin: top left;
transform-box: fill-box;
}
path,
rect {
fill: var(--md-sys-color-background);
fill-opacity: 0;
stroke: currentcolor;
}
path {
fill: currentcolor;
fill-opacity: 0;
stroke-opacity: 0.3;
}
g:hover {
cursor: default;
opacity: 0.6;
transition: opacity #{$transition} ease;
}
g:focus-within {
color: var(--md-sys-color-primary);
outline: none;
> path,
> rect {
fill: currentcolor;
fill-opacity: 0.2;
}
}
</style>

View File

@@ -1,44 +1,58 @@
<script lang="ts">
import {serialPort} from "$lib/serial/connection"
import LayoutCC1 from "$lib/components/layout/LayoutCC1.svelte"
import {action} from "$lib/title"
import GenericLayout from "$lib/components/layout/GenericLayout.svelte"
import {getContext} from "svelte"
import type {Writable} from "svelte/store"
import type {VisualLayout} from "$lib/serialization/visual-layout"
$: device = $serialPort?.device ?? "ONE"
let activeLayer = 0
const activeLayer = getContext<Writable<number>>("active-layer")
const layers = [
["Numeric Layer", "123", 1],
["Primary Layer", "abc", 0],
["Function Layer", "function", 2],
] as const
const layouts = {
ONE: () => import("$lib/assets/layouts/one.yml").then(it => it.default as VisualLayout),
LITE: () => import("$lib/assets/layouts/lite.yml").then(it => it.default as VisualLayout),
X: () => import("$lib/assets/layouts/generic/103-key.yml").then(it => it.default as VisualLayout),
}
</script>
<div>
<select bind:value={device}>
<option value="ONE">CC1</option>
<option value="LITE">Lite</option>
</select>
<div class="container">
<fieldset>
{#each layers as [title, icon, value]}
<button
{title}
class="icon"
on:click={() => (activeLayer = value)}
class:active={activeLayer === value}
use:action={{title, shortcut: `alt+${value + 1}`}}
on:click={() => ($activeLayer = value)}
class:active={$activeLayer === value}
>
{icon}
</button>
{/each}
</fieldset>
{#if device === "ONE"}
<LayoutCC1 bind:activeLayer />
{:else}
<p>Unsupported device ({$serialPort?.device})</p>
{/if}
{#await layouts[device]() then visualLayout}
<GenericLayout {visualLayout} />
{/await}
</div>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin-bottom: 96px;
}
fieldset {
position: relative;
@@ -46,7 +60,6 @@
align-items: center;
justify-content: center;
margin-block-end: -36px;
padding: 0;
border: none;
@@ -76,13 +89,19 @@
outline: 8px solid var(--md-sys-color-background);
}
&:first-child,
&:last-child {
aspect-ratio: unset;
height: unset;
}
&:first-child {
padding-inline-end: 16px;
padding-inline: 4px 16px;
border-radius: 16px 0 0 16px;
}
&:last-child {
padding-inline-start: 16px;
padding-inline: 16px 4px;
border-radius: 0 16px 16px 0;
}

View File

@@ -1,65 +0,0 @@
<script>
import RingInput from "$lib/components/layout/RingInput.svelte"
export let activeLayer = 0
</script>
<div class="col layout" style="gap: 0">
<div class="row" style="gap: 156px">
<div class="row">
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} type="tertiary" />
<div class="col">
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} type="secondary" />
</div>
<div class="col">
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} type="secondary" />
</div>
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
</div>
<div class="row">
<RingInput {activeLayer} keys={{d: 60, w: 61, n: 62, e: 63, s: 64}} />
<div class="col">
<RingInput {activeLayer} keys={{d: 65, w: 66, n: 67, e: 68, s: 69}} />
<RingInput {activeLayer} keys={{d: 80, w: 81, n: 82, e: 83, s: 84}} />
</div>
<div class="col">
<RingInput {activeLayer} keys={{d: 70, w: 71, n: 72, e: 73, s: 74}} />
<RingInput {activeLayer} keys={{d: 85, w: 86, n: 87, e: 88, s: 89}} />
</div>
<RingInput {activeLayer} keys={{d: 75, w: 76, n: 77, e: 78, s: 79}} />
</div>
</div>
<div class="row" style="gap: 48px; margin-top: -32px">
<RingInput {activeLayer} keys={{d: 10, e: 11, n: 12, w: 13, s: 14}} />
<RingInput {activeLayer} keys={{d: 55, w: 56, n: 57, e: 58, s: 59}} />
</div>
<div class="row" style="gap: 160px">
<RingInput {activeLayer} keys={{d: 5, e: 6, n: 7, w: 8, s: 9}} />
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
</div>
<div class="row" style="gap: 320px; margin-top: -12px">
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} type="secondary" />
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} type="secondary" />
</div>
</div>
<style lang="scss">
.row,
.col {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
}
.row {
flex-direction: row;
}
.col {
flex-direction: column;
}
</style>

View File

@@ -1,181 +0,0 @@
<script lang="ts">
import {highlightActions, layout} from "$lib/serial/connection"
import type {CharaLayout} from "$lib/serialization/layout"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import type {KeyInfo} from "$lib/serial/keymap-codes"
import {editableLayout} from "$lib/editable-layout"
export let activeLayer = 0
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
export let type: "primary" | "secondary" | "tertiary" = "primary"
const layerNames = ["Primary Layer", "Number Layer", "Function Layer"]
const virtualLayerMap = [1, 0, 2]
const characterOffset = 8
function offsetDistance(quadrant: number, layer: number, activeLayer: number): number {
const layerOffsetIndex = virtualLayerMap[layer] - virtualLayerMap[activeLayer]
const layerOffset = quadrant > 2 ? -characterOffset : characterOffset
return 25 * quadrant + layerOffsetIndex * layerOffset
}
function getActions(id: number, layout: CharaLayout): KeyInfo[] {
return Array.from({length: 3}).map((_, i) => {
const actionId = layout?.[i][id]
return KEYMAP_CODES[actionId]
})
}
</script>
<div class="radial {type}">
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
{@const actions = getActions(id, $layout)}
<button
use:editableLayout={{id, quadrant}}
class:active={actions.some(it => it && $highlightActions?.includes(it.code))}
>
{#each actions as keyInfo, layer}
{#if keyInfo}
<span
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
class:icon={!!keyInfo.icon}
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
>
{/if}
{/each}
</button>
{/each}
</div>
<style lang="scss">
@use "sass:math";
$border-width: 18px;
$gap: 6px;
$size: 96;
$offset: 14;
$scale-difference: 0.2;
$transition-time: 750ms;
.radial {
position: relative;
container: radial / size;
width: #{$size * 1px};
height: #{$size * 1px};
transition: all 250ms ease;
}
span {
$cr: math.div($size, 2) - 2 * $offset;
will-change: scale, offset-distance;
user-select: none;
scale: 0.9;
offset-path: path(
"M#{math.div($size, 2)} #{$offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$size - $offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$offset}Z"
);
offset-rotate: 0deg;
display: flex;
grid-column: 1;
grid-row: 1;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 16px;
opacity: 0.2;
transition: scale $transition-time ease, opacity $transition-time ease,
offset-distance $transition-time ease;
&.active {
scale: 1;
opacity: 1;
}
&.icon {
font-size: 20px;
font-weight: 800;
}
}
button {
cursor: pointer;
position: absolute;
display: grid;
width: 100cqw;
height: 100cqh;
padding: 0;
font-family: "Noto Sans Mono", monospace;
font-size: 16px;
font-weight: 900;
color: var(--md-sys-color-on-surface-variant);
background: var(--md-sys-color-surface-variant);
border: none;
transition: all 250ms ease;
mask-image: url("$lib/assets/quater-ring.svg");
mask-size: 100% 100%;
&.active,
&:active {
color: var(--md-sys-color-on-tertiary);
background: var(--md-sys-color-tertiary);
}
&:nth-child(1) {
clip-path: polygon(50% 50%, 0 0, 100% 0);
}
&:nth-child(2) {
clip-path: polygon(50% 50%, 100% 0, 100% 100%);
}
&:nth-child(3) {
clip-path: polygon(50% 50%, 0 100%, 100% 100%);
}
&:nth-child(4) {
clip-path: polygon(50% 50%, 0 0, 0 100%);
}
&:last-child {
top: 50%;
left: 50%;
translate: -50% -50%;
overflow: hidden;
width: 25cqw;
height: 25cqh;
border-radius: 50%;
mask-image: none;
}
}
.secondary > button {
filter: brightness(80%) contrast(120%);
}
.tertiary > button {
filter: brightness(80%) contrast(110%);
}
</style>

View File

@@ -0,0 +1,9 @@
export interface VisualLayoutConfig {
scale: number
inactiveScale: number
inactiveOpacity: number
strokeWidth: number
margin: number
fontSize: number
iconFontSize: number
}

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import {createEventDispatcher} from "svelte"
import Dialog from "$lib/dialogs/Dialog.svelte"
export let title: string
export let message: string | undefined
export let abortTitle: string
export let confirmTitle: string
const dispatch = createEventDispatcher()
</script>
<Dialog>
<h1>{@html title}</h1>
{#if message}
<p>{@html message}</p>
{/if}
<div class="buttons">
<button on:click={() => dispatch("abort")}>{abortTitle}</button>
<button class="primary" on:click={() => dispatch("confirm")}>{confirmTitle}</button>
</div>
</Dialog>
<style lang="scss">
h1 {
font-size: 2em;
text-align: center;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import {onMount} from "svelte"
onMount(() => {
modal.showModal()
})
let modal: HTMLDialogElement
</script>
<dialog bind:this={modal}>
<slot />
</dialog>
<style lang="scss">
dialog {
min-width: 300px;
max-width: 512px;
color: var(--md-sys-color-on-background);
background: var(--md-sys-color-background);
border: none;
border-radius: 38px;
box-shadow: 0 0 48px rgba(0 0 0 / 60%);
}
dialog::backdrop {
opacity: 0.5;
background: black;
}
</style>

View File

@@ -0,0 +1,161 @@
<script lang="ts">
import Dialog from "$lib/dialogs/Dialog.svelte"
import type {Change, ChordChange, LayoutChange, SettingChange} from "$lib/undo-redo"
import {ChangeType, chords} from "$lib/undo-redo"
import ActionString from "$lib/components/ActionString.svelte"
import LL from "../../i18n/i18n-svelte"
import {KEYMAP_IDS} from "$lib/serial/keymap-codes"
export let changes: Change[] = [
{type: ChangeType.Layout, layer: 0, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Chord, id: [1], actions: [55], phrase: [55, 63, 37, 36]},
{
type: ChangeType.Chord,
id: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
actions: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
phrase: [55, 63, 37, 36],
},
{
type: ChangeType.Chord,
id: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
actions: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
phrase: [],
},
]
$: existingChords = new Set($chords.map(it => JSON.stringify(it.id)))
$: layoutChanges = Array.from(
{length: 3},
(_, i) => changes.filter(it => it.type === ChangeType.Layout && it.layer === i) as LayoutChange[],
)
$: settingChanges = changes.filter(it => it.type === ChangeType.Setting) as SettingChange[]
$: chordChanges = {
added: changes.filter(
it =>
it.type === ChangeType.Chord && it.phrase.length > 0 && !existingChords.has(JSON.stringify(it.id)),
) as ChordChange[],
changed: changes.filter(
it => it.type === ChangeType.Chord && it.phrase.length > 0 && existingChords.has(JSON.stringify(it.id)),
) as ChordChange[],
deleted: changes.filter(it => it.type === ChangeType.Chord && it.phrase.length === 0) as ChordChange[],
}
$: totalChordChanges = Object.values(chordChanges).reduce((acc, curr) => acc + curr.length, 0)
</script>
<Dialog>
<h1>{$LL.changes.TITLE()}</h1>
<h2>
<label><input type="checkbox" class="checkbox" />{$LL.changes.ALL_CHANGES()}</label>
</h2>
<ul>
{#if layoutChanges.some(it => it.length > 0)}
<li>
<h3>
<label>
<input type="checkbox" class="checkbox" />
{$LL.changes.layout.TITLE(layoutChanges.reduce((acc, curr) => acc + curr.length, 0))}
</label>
</h3>
<ul>
{#each layoutChanges
.map((it, i) => /** @type {const} */ ([it, i + 1]))
.filter(([it]) => it.length > 0) as [changes, layer]}
<li>
<h4>
<label>
<input type="checkbox" class="checkbox" />
{$LL.changes.layout.LAYER({changes: changes.length, layer})}
</label>
</h4>
</li>
{/each}
</ul>
</li>
{/if}
{#if settingChanges.length > 0}
<li>
<h3>
<label
><input type="checkbox" class="checkbox" />{$LL.changes.settings.TITLE(
settingChanges.length,
)}</label
>
</h3>
</li>
{/if}
{#if totalChordChanges > 0}
<li>
<h3>
<label
><input type="checkbox" class="checkbox" />{$LL.changes.chords.TITLE(totalChordChanges)}</label
>
</h3>
<ul>
{#each Object.entries(chordChanges) as [category, changes]}
{#if changes.length > 0}
<li>
<h4>
<label
><input type="checkbox" class="checkbox" />
{#if category === "added"}
{$LL.changes.chords.NEW_CHORDS(changes.length)}
{:else if category === "changed"}
{$LL.changes.chords.CHANGED_CHORDS(changes.length)}
{:else if category === "deleted"}
{$LL.changes.chords.DELETED_CHORDS(changes.length)}
{/if}
</label>
</h4>
<ul>
{#each changes as change}
<li>
<label>
<input type="checkbox" class="checkbox" />
<ActionString display="keys" actions={change.actions} />
<ActionString actions={change.phrase} />
</label>
</li>
{/each}
</ul>
</li>
{/if}
{/each}
</ul>
</li>
{/if}
</ul>
</Dialog>
<style lang="scss">
h1 {
font-size: 2em;
text-align: center;
}
h2 {
font-size: 1.5em;
}
ul {
padding-inline-start: 0;
list-style: none;
}
li {
margin-inline-start: 24px;
}
</style>

View File

@@ -0,0 +1,31 @@
import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte"
export async function askForConfirmation(
title: string,
message: string,
confirmTitle: string,
abortTitle: string,
): Promise<boolean> {
const dialog = new ConfirmDialog({
target: document.body,
props: {
title,
message,
confirmTitle,
abortTitle,
},
})
let resolvePromise: (value: boolean) => void
const resultPromise = new Promise<boolean>(resolve => {
resolvePromise = resolve
})
dialog.$on("abort", () => resolvePromise(false))
dialog.$on("confirm", () => resolvePromise(true))
const result = await resultPromise
dialog.$destroy()
return result
}

View File

@@ -1,32 +0,0 @@
import tippy from "tippy.js"
import InputEdit from "$lib/components/layout/InputEdit.svelte"
import type {Action} from "svelte/action"
export const editableLayout: Action<HTMLButtonElement, {id: number; quadrant: number}> = (
node,
{id, quadrant},
) => {
let component: InputEdit | undefined
const edit = tippy(node, {
interactive: true,
appendTo: document.body,
trigger: "click",
placement: (["top", "right", "bottom", "left"] as const)[quadrant],
onShow(instance) {
component ??= new InputEdit({
target: instance.popper.querySelector(".tippy-content")!,
props: {id},
})
},
onHidden() {
component?.$destroy()
component = undefined
},
})
return {
destroy() {
edit.destroy()
},
}
}

View File

@@ -15,7 +15,9 @@
font-family: "Material Symbols Rounded";
font-size: 24px;
font-feature-settings: "liga";
font-variation-settings: "FILL" var(--icon-fill, 0), "wght" var(--icon-weigth, 400),
font-variation-settings:
"FILL" var(--icon-fill, 0),
"wght" var(--icon-weigth, 400),
"GRAD" var(--icon-grade, 0);
font-weight: normal;
font-style: normal;

25
src/lib/os-layout.ts Normal file
View File

@@ -0,0 +1,25 @@
import {get, writable} from "svelte/store"
export const osLayout = writable<Map<string, string>>(new Map())
async function updateLayout() {
const layout: Map<string, string> = await (navigator as any).keyboard.getLayoutMap()
const currentLayout = get(osLayout)
if (
layout.size !== currentLayout.size ||
[...layout.keys()].some(key => layout.get(key) !== currentLayout.get(key))
) {
osLayout.set(layout)
}
}
export function runLayoutDetection(): () => void {
if ("keyboard" in navigator) {
updateLayout()
const timer = setInterval(updateLayout, 5000)
return () => clearInterval(timer)
} else {
console.warn("Keyboard API not supported")
return () => {}
}
}

View File

@@ -1,5 +1,5 @@
import type {Action} from "svelte/action"
import {persistentWritable} from "$lib/storage"
import type { Action } from "svelte/action"
import { persistentWritable } from "$lib/storage"
export interface UserPreferences {
backup: boolean
@@ -13,7 +13,7 @@ export const theme = persistentWritable("user-theme", {
export const userPreferences = persistentWritable<UserPreferences>("user-preferences", {
backup: false,
autoConnect: true,
autoConnect: false,
})
export const preference: Action<HTMLInputElement, keyof UserPreferences> = (node, key) => {

View File

@@ -5,6 +5,7 @@ import type {Writable} from "svelte/store"
import type {CharaLayout} from "$lib/serialization/layout"
import {persistentWritable} from "$lib/storage"
import {userPreferences} from "$lib/preferences"
import settingInfo from "$lib/assets/settings.yml"
export const serialPort = writable<CharaDevice | undefined>()
@@ -15,41 +16,86 @@ export interface SerialLogEntry {
export const serialLog = writable<SerialLogEntry[]>([])
export const chords = persistentWritable<Chord[]>("chord-library", [], () => get(userPreferences).backup)
/**
* Chords as read from the device
*/
export const deviceChords = persistentWritable<Chord[]>(
"chord-library",
[],
() => get(userPreferences).backup,
)
export const layout = persistentWritable<CharaLayout>(
/**
* Layout as read from the device
*/
export const deviceLayout = persistentWritable<CharaLayout>(
"layout",
[[], [], []],
() => get(userPreferences).backup,
)
export const settings = writable({})
export const unsavedChanges = writable(0)
export const highlightActions: Writable<number[]> = writable([])
/**
* Settings as read from the device
*/
export const deviceSettings = persistentWritable<number[]>(
"device-settings",
[],
() => get(userPreferences).backup,
)
export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done")
export interface ProgressInfo {
max: number
current: number
}
export const syncProgress = writable<ProgressInfo | undefined>(undefined)
export async function initSerial(manual = false) {
const device = get(serialPort) ?? new CharaDevice()
await device.init(manual)
serialPort.set(device)
sync()
}
export async function sync() {
const device = get(serialPort)
if (!device) return
const chordCount = await device.getChordCount()
syncStatus.set("downloading")
const max = Object.keys(settingInfo.settings).length + device.keyCount * 3 + chordCount
let current = 0
syncProgress.set({max, current})
function progressTick() {
current++
syncProgress.set({max, current})
}
const parsedSettings: number[] = []
for (const key in settingInfo.settings) {
try {
parsedSettings[Number.parseInt(key)] = await device.getSetting(Number.parseInt(key))
} catch {}
progressTick()
}
deviceSettings.set(parsedSettings)
const parsedLayout: CharaLayout = [[], [], []]
for (let layer = 1; layer <= 3; layer++) {
for (let i = 0; i < 90; i++) {
for (let i = 0; i < device.keyCount; i++) {
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i)
progressTick()
}
}
layout.set(parsedLayout)
deviceLayout.set(parsedLayout)
const chordCount = await device.getChordCount()
const chordInfo = []
for (let i = 0; i < chordCount; i++) {
chordInfo.push(await device.getChord(i))
progressTick()
}
chords.set(chordInfo)
deviceChords.set(chordInfo)
syncStatus.set("done")
syncProgress.set(undefined)
}

View File

@@ -1,17 +1,39 @@
import {LineBreakTransformer} from "$lib/serial/line-break-transformer"
import {serialLog} from "$lib/serial/connection"
import type {Chord} from "$lib/serial/chord"
import {SemVer} from "$lib/serial/sem-ver"
import {parseChordActions, parsePhrase, stringifyChordActions, stringifyPhrase} from "$lib/serial/chord"
import {browser} from "$app/environment"
export const VENDOR_ID = 0x239a
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
["ONE M0", {usbProductId: 32783, usbVendorId: 9114}],
["LITE S2", {usbProductId: 33070, usbVendorId: 12346}],
["LITE M0", {usbProductId: 32796, usbVendorId: 9114}],
["X", {usbProductId: 33163, usbVendorId: 12346}],
])
const KEY_COUNTS = {
ONE: 90,
LITE: 67,
X: 256,
} as const
if (browser && navigator.serial === undefined && import.meta.env.TAURI_FAMILY !== undefined) {
await import("./tauri-serial")
}
export async function getViablePorts(): Promise<SerialPort[]> {
return navigator.serial.getPorts().then(ports => ports.filter(it => it.getInfo().usbVendorId === VENDOR_ID))
return navigator.serial.getPorts().then(ports =>
ports.filter(it => {
const {usbProductId, usbVendorId} = it.getInfo()
for (const filter of PORT_FILTERS.values()) {
if (filter.usbProductId === usbProductId && filter.usbVendorId === usbVendorId) {
return true
}
}
return false
}),
)
}
export async function canAutoConnect() {
@@ -29,31 +51,66 @@ export class CharaDevice {
private lock?: Promise<true>
version!: [number, number, number]
private readonly suspendDebounce = 100
private suspendDebounceId?: number
version!: SemVer
company!: "CHARACHORDER"
device!: "ONE" | "LITE"
device!: "ONE" | "LITE" | "X"
chipset!: "M0" | "S2"
keyCount!: 90 | 67 | 256
get portInfo() {
return this.port.getInfo()
}
constructor(private readonly baudRate = 115200) {}
async init(manual = false) {
const ports = await getViablePorts()
this.port =
!manual && ports.length === 1
? ports[0]
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
await this.port.open({baudRate: this.baudRate})
const info = this.port.getInfo()
serialLog.update(it => {
it.push({
type: "system",
value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString(
16,
)}`,
})
return it
})
try {
const ports = await getViablePorts()
this.port =
!manual && ports.length === 1
? ports[0]
: await navigator.serial.requestPort({filters: [...PORT_FILTERS.values()]})
await this.port.open({baudRate: this.baudRate})
const info = this.port.getInfo()
serialLog.update(it => {
it.push({
type: "system",
value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString(
16,
)}`,
})
return it
})
await this.port.close()
this.version = new SemVer(await this.send("VERSION").then(([version]) => version))
const [company, device, chipset] = await this.send("ID")
this.company = company as "CHARACHORDER"
this.device = device as "ONE" | "LITE" | "X"
this.chipset = chipset as "M0" | "S2"
this.keyCount = KEY_COUNTS[this.device]
} catch (e) {
alert(e)
console.error(e)
throw e
}
}
private async suspend() {
await this.reader.cancel()
await this.streamClosed.catch(() => {
/** noop */
})
this.reader.releaseLock()
await this.port.close()
}
private async wake() {
await this.port.open({baudRate: this.baudRate})
const decoderStream = new TextDecoderStream()
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
signal: this.abortController1.signal,
@@ -64,13 +121,6 @@ export class CharaDevice {
signal: this.abortController2.signal,
})
.getReader()
const [version] = await this.send("VERSION")
this.version = version.split(".").map(Number) as [number, number, number]
const [company, device, chipset] = await this.send("ID")
this.company = company as "CHARACHORDER"
this.device = device as "ONE" | "LITE"
this.chipset = chipset as "M0" | "S2"
}
private async internalRead() {
@@ -105,19 +155,9 @@ export class CharaDevice {
}
async forget() {
await this.disconnect()
await this.port.forget()
}
async disconnect() {
await this.reader.cancel()
await this.streamClosed.catch(() => {
/** noop */
})
this.reader.releaseLock()
await this.port.close()
}
/**
* Read/write to serial port
*/
@@ -132,9 +172,23 @@ export class CharaDevice {
const exec = new Promise<T>(async resolve => {
let result!: T
try {
if (this.suspendDebounceId) {
clearTimeout(this.suspendDebounceId)
} else {
await this.wake()
}
result = await callback(send, read)
} finally {
this.lock = undefined
delete this.lock
this.suspendDebounceId = setTimeout(() => {
// cannot be locked here as all the code until clearTimeout is sync
console.assert(this.lock === undefined)
this.lock = this.suspend().then(() => {
delete this.lock
delete this.suspendDebounceId
return true
})
}, this.suspendDebounce) as any
resolve(result)
}
})
@@ -174,7 +228,7 @@ export class CharaDevice {
*/
async getChordPhrase(actions: number[]): Promise<number[] | undefined> {
const [phrase] = await this.send(`CML C2 ${stringifyChordActions(actions)}`)
return phrase === "0" ? undefined : parsePhrase(phrase)
return phrase === "2" ? undefined : parsePhrase(phrase)
}
async setChord(chord: Chord) {
@@ -184,12 +238,13 @@ export class CharaDevice {
stringifyChordActions(chord.actions),
stringifyPhrase(chord.phrase),
)
if (status !== "0") throw new Error(`Failed with status ${status}`)
if (status !== "0") console.error(`Failed with status ${status}`)
}
async deleteChord(chord: Chord) {
async deleteChord(chord: Pick<Chord, "actions">) {
const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`)
if (status.at(-1) !== "0") throw new Error(`Failed with status ${status}`)
console.log(status)
if (status.at(-1) !== "2") throw new Error(`Failed with status ${status}`)
}
/**
@@ -199,7 +254,8 @@ export class CharaDevice {
* @param action the assigned action id
*/
async setLayoutKey(layer: number, id: number, action: number) {
const [status] = await this.send(`VAR B3 A${layer} ${id} ${action}`)
const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`)
console.log(status)
if (status !== "0") throw new Error(`Failed with status ${status}`)
}
@@ -234,7 +290,7 @@ export class CharaDevice {
* To permanently store the settings, you *must* call commit.
*/
async setSetting(id: number, value: number) {
const [status] = await this.send(`VAR B2 ${id} ${value}`)
const [status] = await this.send(`VAR B2 ${id.toString(16).toUpperCase()} ${value}`)
if (status !== "0") throw new Error(`Failed with status ${status}`)
}
@@ -242,8 +298,9 @@ export class CharaDevice {
* Retrieves a setting from the device
*/
async getSetting(id: number): Promise<number> {
const [value, status] = await this.send(`VAR B1 ${id}`)
if (status !== "0") throw new Error(`Setting "${id}" doesn't exist (Status code ${status})`)
const [value, status] = await this.send(`VAR B1 ${id.toString(16).toUpperCase()}`)
if (status !== "0")
throw new Error(`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`)
return Number(value)
}
@@ -252,8 +309,6 @@ export class CharaDevice {
*/
async reboot() {
await this.send("RST")
await this.disconnect()
// TODO: reconnect
}
/**
@@ -261,8 +316,13 @@ export class CharaDevice {
*/
async bootloader() {
await this.send("RST BOOTLOADER")
await this.disconnect()
// TODO: more...
}
/**
* Resets the device
*/
async reset(type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC") {
await this.send(`RST ${type}`)
}
/**

View File

@@ -5,17 +5,31 @@ export interface KeyInfo extends Partial<ActionInfo> {
category: KeymapCategory
}
const keymaps = (await Promise.all(
export const KEYMAP_CATEGORIES = (await Promise.all(
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(async load =>
load().then(it => (it as any).default),
),
)) as KeymapCategory[]
export const KEYMAP_CODES: Record<number, KeyInfo> = Object.fromEntries(
keymaps.flatMap(category =>
KEYMAP_CATEGORIES.flatMap(category =>
Object.entries(category.actions).map(([code, action]) => [
Number(code),
{...action, code: Number(code), category},
]),
),
)
export const KEYMAP_KEYCODES: Map<string, number> = new Map(
KEYMAP_CATEGORIES.flatMap(category =>
Object.entries(category.actions).map(([code, action]) => [action.keyCode!, Number(code)] as const),
).filter(([keyCode]) => keyCode !== undefined),
)
export const KEYMAP_IDS: Map<string, KeyInfo> = new Map(
KEYMAP_CATEGORIES.flatMap(category =>
Object.entries(category.actions).map(
([code, action]) => [action.id!, {...action, code: Number(code), category}] as const,
),
).filter(([id]) => id !== undefined),
)

27
src/lib/serial/sem-ver.ts Normal file
View File

@@ -0,0 +1,27 @@
export class SemVer {
major: number
minor: number
patch: number
preRelease?: string
meta?: string
constructor(versionString: string) {
const [, major, minor, patch, preRelease, meta] =
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+))?$/.exec(
versionString,
)!
this.major = Number.parseInt(major)
this.minor = Number.parseInt(minor)
this.patch = Number.parseInt(patch)
if (preRelease) this.preRelease = preRelease
if (meta) this.meta = meta
}
toString() {
return (
`${this.major}.${this.minor}.${this.patch}` +
(this.preRelease ? `-${this.preRelease}` : "") +
(this.meta ? `+${this.meta}` : "")
)
}
}

12
src/lib/serial/updater.ts Normal file
View File

@@ -0,0 +1,12 @@
export async function updateDevice(port: SerialPort) {
await port.open({
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none",
bufferSize: 255,;
})
const writer = port.writable!.getWriter()
const reader = port.readable!.getReader()
}

View File

@@ -24,7 +24,7 @@ export function decompressActions(raw: Uint8Array): number[] {
const actions: number[] = []
for (let i = 0; i < raw.length; i++) {
let action = raw[i]
if (action < 32) {
if (action > 0 && action < 32) {
action = (action << 8) | raw[++i]
}
actions.push(action)

View File

@@ -13,17 +13,16 @@ export async function toBase64(blob: Blob): Promise<string> {
.replace(/^data:application\/octet-stream;base64,/, "")
.replaceAll("+", ".")
.replaceAll("/", "_")
.replaceAll("=", "-")}-`,
.replaceAll("=", "-")}`,
)
}
reader.readAsDataURL(blob)
})
}
export async function fromBase64(base64: string): Promise<Blob> {
export async function fromBase64(base64: string, fetch = window.fetch): Promise<Blob> {
return fetch(
`data:application/octet-stream;base64,${base64
.replace(/-$/, "")
.replaceAll(".", "+")
.replaceAll("_", "/")
.replaceAll("-", "=")}`,

View File

@@ -1,6 +1,15 @@
import {compressActions, decompressActions} from "./actions"
import {fromBase64, toBase64} from "$lib/serialization/base64"
export interface NewCharaLayout {
charaLayoutVersion: 1
device: "one" | "lite" | string
/**
* Layers A1-A3, with numeric action codes on each
*/
layers: [number[], number[], number[]]
}
export type CharaLayout = [number[], number[], number[]]
/**

View File

@@ -0,0 +1,106 @@
export interface VisualLayout {
name: string
col: VisualLayoutRow[]
}
interface Positionable {
offset: [number, number]
rotate: number
}
export interface VisualLayoutRow extends Positionable {
row: Array<VisualLayoutKey | VisualLayoutSwitch>
}
export interface VisualLayoutKey extends Positionable {
key: number
size?: [number, number]
}
export interface VisualLayoutSwitch extends Positionable {
switch: {
n: number
e: number
w: number
s: number
d: number
}
}
export interface CompiledLayout {
name: string
size: [number, number]
keys: CompiledLayoutKey[]
}
export interface CompiledLayoutKey {
id: number
shape: "quarter-circle" | "square"
cornerRadius: number
size: [number, number]
pos: [number, number]
rotate: number
}
export function compileLayout(layout: VisualLayout): CompiledLayout {
const compiled: CompiledLayout = {
name: layout.name,
size: [0, 0],
keys: [],
}
let y = 0
for (const {row, offset} of layout.col) {
let x = offset?.[0] ?? 0
y += offset?.[1] ?? 0
let maxHeight = 0
for (const info of row) {
const [ox, oy] = info.offset || [0, 0]
const rotate = info.rotate || 0
if ("key" in info) {
const [width, height] = info.size ?? [1, 1]
compiled.keys.push({
id: info.key,
shape: "square",
size: [width, height],
pos: [x + ox, y + oy],
cornerRadius: 0.1,
rotate,
})
x += width + ox
maxHeight = Math.max(maxHeight, height + oy)
} else if ("switch" in info) {
const cx = x + ox + 1
const cy = y + oy + 1
for (const [i, id] of [info.switch.s, info.switch.w, info.switch.n, info.switch.e].entries()) {
compiled.keys.push({
id,
shape: "quarter-circle",
cornerRadius: 0,
size: [2, 0.6],
pos: [cx, cy],
rotate: 90 * i + 45,
})
}
compiled.keys.push({
id: info.switch.d,
shape: "square",
cornerRadius: 0.5,
size: [0.8, 0.8],
pos: [x + 0.6 + ox, y + 0.6 + oy],
rotate: 0,
})
x += 2 + ox
maxHeight = Math.max(maxHeight, 2 + oy)
}
}
y += maxHeight
compiled.size[0] = Math.max(compiled.size[0], x)
}
compiled.size[1] = y
return compiled
}

View File

@@ -1,34 +1,66 @@
import type {Action} from "svelte/action"
import {serialPort} from "$lib/serial/connection"
import {changes, ChangeType, settings} from "$lib/undo-redo"
export const setting: Action<HTMLInputElement, {id: number; inverse?: number; scale?: number}> = function (
node: HTMLInputElement,
{id, inverse, scale},
) {
node.setAttribute("disabled", "")
const type = node.getAttribute("type") as "number" | "checkbox"
const min = node.hasAttribute("min") ? Number(node.getAttribute("min")) : undefined
const max = node.hasAttribute("max") ? Number(node.getAttribute("max")) : undefined
console.log(min, max, "|", id, "|", node.getAttribute("min"), node.getAttribute("max"))
const unsubscribe = serialPort.subscribe(async port => {
if (port) {
const type = node.getAttribute("type") as "number" | "checkbox"
const unsubscribe = settings.subscribe(async settings => {
if (id in settings) {
const {value, isApplied} = settings[id]
if (type === "number") {
const value = Number(await port.getSetting(id).then(it => it.toString()))
node.value = (
inverse !== undefined ? inverse / value : scale !== undefined ? scale * value : value
).toString()
} else {
node.checked = await port.getSetting(id).then(it => it !== 0)
node.checked = value !== 0
}
if (isApplied) {
node.classList.remove("pending-changes")
} else {
node.classList.add("pending-changes")
}
node.removeAttribute("disabled")
} else {
node.setAttribute("disabled", "")
}
})
function listener() {}
node.addEventListener("input", listener)
async function listener() {
let value: number
if (type === "number") {
value = Number(node.value)
if (Number.isNaN(value)) return
value = Math.floor(
inverse !== undefined ? inverse / value : scale !== undefined ? value / scale : value,
)
if (min !== undefined) value = Math.max(min, value)
if (max !== undefined) value = Math.min(max, value)
} else {
value = node.checked ? 1 : 0
}
changes.update(changes => {
changes.push({
type: ChangeType.Setting,
id: id,
setting: value,
})
return changes
})
}
node.addEventListener("change", listener)
return {
destroy() {
node.removeEventListener("input", listener)
node.removeEventListener("change", listener)
unsubscribe()
},
}

View File

@@ -0,0 +1,53 @@
import {describe, it, expect} from "vitest"
import {deserializeActionArray, serializeActionArray} from "./action-array"
describe("action array", () => {
it("should work with number arrays", () => {
expect(deserializeActionArray(serializeActionArray([62, 256, 1235]))).toEqual([62, 256, 1235])
})
it("should work with nested arrays", () => {
expect(deserializeActionArray(serializeActionArray([[], [[]]]))).toEqual([[], [[]]])
})
it("should compress back and forth", () => {
expect(
deserializeActionArray(
serializeActionArray([
[43, 746, 634],
[34, 63],
[332, 34],
]),
),
).toEqual([
[43, 746, 634],
[34, 63],
[332, 34],
])
})
it("should compress a full layout", () => {
const layout = Object.freeze([
Object.freeze([
0, 0, 0, 0, 0, 53, 119, 45, 103, 122, 52, 107, 118, 109, 99, 51, 114, 36, 59, 101, 50, 105, 34, 46,
111, 49, 39, 515, 44, 117, 0, 512, 514, 513, 550, 0, 319, 318, 321, 320, 326, 315, 314, 317, 316, 0,
0, 0, 0, 0, 54, 98, 120, 536, 113, 55, 102, 112, 104, 100, 56, 97, 296, 544, 116, 57, 108, 299, 106,
110, 48, 121, 297, 61, 115, 0, 518, 516, 517, 553, 0, 336, 338, 335, 337, 0, 325, 322, 323, 324,
]),
Object.freeze([
0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 53, 0, 47, 52, 0, 51, 298, 0, 50, 0, 0, 127, 0, 49, 0, 0, 515, 0, 0,
0, 512, 514, 513, 550, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 0, 536, 0, 0, 54, 0, 92,
55, 0, 56, 296, 544, 57, 0, 96, 299, 0, 48, 0, 0, 297, 0, 0, 0, 518, 516, 517, 553, 0, 336, 338, 335,
337, 0, 0, 0, 0, 0,
]),
Object.freeze([
0, 0, 0, 0, 0, 0, 64, 95, 43, 0, 0, 126, 38, 63, 40, 0, 35, 298, 36, 123, 0, 33, 127, 37, 60, 0, 34,
515, 0, 0, 0, 512, 514, 513, 550, 0, 333, 331, 330, 334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 536,
0, 0, 94, 58, 124, 41, 0, 42, 296, 544, 125, 0, 126, 299, 0, 62, 0, 0, 297, 0, 0, 0, 518, 516, 517,
553, 0, 336, 338, 335, 337, 0, 0, 0, 0, 0,
]),
])
expect(deserializeActionArray(serializeActionArray(layout as number[][]))).toEqual(layout)
})
})

View File

@@ -0,0 +1,55 @@
import {compressActions, decompressActions} from "../serialization/actions"
import {CHARA_FILE_TYPES} from "../share/share-url"
export type ActionArray = number[] | ActionArray[]
export function serializeActionArray(array: ActionArray): Uint8Array {
let out = new Uint8Array(5)
const writer = new DataView(out.buffer)
writer.setUint32(0, array.length)
if (array.length === 0) {
return out
} else if (typeof array[0] === "number") {
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("number"))
const compressed = compressActions(array as number[])
writer.setUint32(0, compressed.length)
return concatUint8Arrays(out, compressed)
} else if (Array.isArray(array[0])) {
writer.setUint8(4, CHARA_FILE_TYPES.indexOf("array"))
return concatUint8Arrays(out, ...(array as ActionArray[]).map(serializeActionArray))
} else {
throw new Error("Not implemented")
}
}
export function deserializeActionArray(raw: Uint8Array, cursor = {pos: 0}): ActionArray {
const reader = new DataView(raw.buffer)
const length = reader.getUint32(cursor.pos)
cursor.pos += 4
const type = CHARA_FILE_TYPES[reader.getUint8(cursor.pos)]
cursor.pos++
if (type === "number") {
const decompressed = decompressActions(raw.slice(cursor.pos, cursor.pos + length))
cursor.pos += length
return decompressed
} else if (type === "array") {
const out = []
for (let i = 0; i < length; i++) {
out.push(deserializeActionArray(raw, cursor))
}
return out
} else {
throw new Error("Not implemented")
}
}
export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
const out = new Uint8Array(arrays.reduce((a, b) => a + b.length, 0))
let offset = 0
for (const array of arrays) {
out.set(array, offset)
offset += array.length
}
return out
}

View File

@@ -0,0 +1,23 @@
export interface CharaFile<T extends string> {
charaVersion: 1
type: T
}
export interface CharaLayoutFile extends CharaFile<"layout"> {
device?: "ONE" | "LITE" | string
layout: [number[], number[], number[]]
}
export interface CharaChordFile extends CharaFile<"chords"> {
chords: [number[], number[]][]
}
export interface CharaSettingsFile extends CharaFile<"settings"> {
settings: number[]
}
export interface CharaBackupFile extends CharaFile<"backup"> {
history: [CharaChordFile, CharaLayoutFile, CharaSettingsFile][]
}
export type CharaFiles = CharaLayoutFile | CharaChordFile | CharaSettingsFile

View File

@@ -0,0 +1,66 @@
import type {CharaFile, CharaFiles} from "../share/chara-file"
import type {ActionArray} from "../share/action-array"
import {deserializeActionArray, serializeActionArray} from "../share/action-array"
import {fromBase64, toBase64} from "../serialization/base64"
type CharaLayoutOrder = {
[K in CharaFiles["type"]]: Array<
[Exclude<keyof Extract<CharaFiles, {type: K}>, keyof CharaFile<any>>, (typeof CHARA_FILE_TYPES)[number]]
>
}
const keys: CharaLayoutOrder = {
layout: [
["layout", "array"],
["device", "string"],
],
chords: [["chords", "array"]],
settings: [["settings", "array"]],
}
export const CHARA_FILE_TYPES = ["unknown", "number", "string", "array"] as const
const sep = "\n"
export async function charaFileToUriComponent<T extends CharaFiles>(file: T): Promise<string> {
let url = `${file.type}${sep}${file.charaVersion}`
for (const [key, type] of keys[file.type]) {
const value = file[key as keyof T]
url += sep
if (type === "string") {
url += value as string
} else if (type === "array") {
const stream = new Blob([serializeActionArray(value as ActionArray)])
.stream()
.pipeThrough(new CompressionStream("deflate"))
url += await toBase64(await new Response(stream).blob())
} else {
throw new Error("Not implemented")
}
}
return url
}
export async function charaFileFromUriComponent<T extends CharaFiles>(
uriComponent: string,
fetch = window.fetch,
): Promise<T> {
const [fileType, version, ...values] = uriComponent.split(sep)
const file: any = {type: fileType, charaVersion: Number(version)}
for (const [key, type] of keys[fileType as keyof typeof keys]) {
const value = values.shift()!
if (type === "string") {
file[key] = value
} else if (type === "array") {
const stream = (await fromBase64(value, fetch)).stream().pipeThrough(new DecompressionStream("deflate"))
const actions = new Uint8Array(await new Response(stream).arrayBuffer())
console.log(actions)
file[key] = deserializeActionArray(actions)
}
}
return file
}

35
src/lib/style/_kbd.scss Normal file
View File

@@ -0,0 +1,35 @@
kbd {
display: inline-flex;
align-items: center;
justify-content: center;
height: 20px;
margin-block: 6px;
padding: 4px;
font-size: 14px;
font-weight: normal;
color: currentcolor;
border: 1px solid currentcolor;
border-radius: 4px;
&.icon {
padding: 2px;
font-size: 18px;
}
&:has(> kbd) {
gap: 4px;
padding: 0;
border: none;
}
> kbd {
padding: 2px;
&.icon {
padding: 0;
}
}
}

View File

@@ -0,0 +1,75 @@
a {
text-decoration: none;
}
a,
label:has(input),
button {
cursor: pointer;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
width: max-content;
height: 48px;
padding-block: 8px;
padding-inline: 16px;
font-family: inherit;
font-weight: 600;
color: currentcolor;
background: transparent;
border: none;
border-radius: 32px;
transition: all 250ms ease;
&.icon {
display: inline-flex;
aspect-ratio: 1;
padding-block: 0;
padding-inline: 0;
font-size: 24px;
border-radius: 50%;
}
&.primary {
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
}
&.compact {
height: 32px;
}
}
label:has(input):hover,
.button:hover:not(:active),
a:hover:not(:active),
button:hover:not(:active) {
filter: brightness(70%);
transition: filter 250ms ease;
&:has(:checked),
&.active {
filter: brightness(120%);
}
&:disabled,
&.disabled {
opacity: 0.5;
filter: none;
}
}
.disabled,
:disabled {
pointer-events: none;
opacity: 0.5;
}

16
src/lib/style/print.scss Normal file
View File

@@ -0,0 +1,16 @@
@media print {
.print {
visibility: visible;
}
nav {
display: none;
}
body {
--md-sys-color-background: white !important;
--md-sys-color-on-background: black !important;
visibility: hidden;
}
}

10
src/lib/style/theme.scss Normal file
View File

@@ -0,0 +1,10 @@
@import "./form/button";
@import "./form/toggle";
@import "./form/checkbox";
@import "./kbd";
@import "./print";
* {
box-sizing: border-box;
appearance: none;
}

View File

@@ -24,6 +24,13 @@ $padding: 16px;
}
}
.tippy-box[data-theme~="tooltip"] {
color: var(--md-sys-color-on-background);
background-color: var(--md-sys-color-background);
border: 1px solid var(--md-sys-color-outline);
border-radius: 8px;
}
.tippy-box[data-theme~="search-completion"] {
overflow: hidden;
filter: none;

45
src/lib/title.ts Normal file
View File

@@ -0,0 +1,45 @@
import type {Action} from "svelte/action"
import tippy from "tippy.js"
import type {SvelteComponent} from "svelte"
import Tooltip from "$lib/components/Tooltip.svelte"
import hotkeys from "hotkeys-js"
export const action: Action<Element, {title?: string; shortcut?: string}> = (
node: Element,
{title, shortcut},
) => {
let component: SvelteComponent | undefined
const tooltip = tippy(node, {
arrow: false,
theme: "tooltip",
animation: "fade",
onShow(instance) {
component ??= new Tooltip({
target: instance.popper.querySelector(".tippy-content") as Element,
props: {title, shortcut},
})
},
onHidden() {
component?.$destroy()
component = undefined
},
})
if (shortcut && node instanceof HTMLElement) {
hotkeys(shortcut, function (keyboardEvent) {
keyboardEvent.preventDefault()
node.click()
})
}
return {
update(updated) {
title = updated.title
shortcut = updated.shortcut
},
destroy() {
tooltip.destroy()
hotkeys.unbind(shortcut)
},
}
}

147
src/lib/undo-redo.ts Normal file
View File

@@ -0,0 +1,147 @@
import {persistentWritable} from "$lib/storage"
import {derived} from "svelte/store"
import type {Chord} from "$lib/serial/chord"
import {deviceChords, deviceLayout, deviceSettings} from "$lib/serial/connection"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
export enum ChangeType {
Layout,
Chord,
Setting,
}
export interface LayoutChange {
type: ChangeType.Layout
id: number
layer: number
action: number
}
export interface ChordChange {
type: ChangeType.Chord
deleted?: true
id: number[]
actions: number[]
phrase: number[]
}
export interface SettingChange {
type: ChangeType.Setting
id: number
setting: number
}
export interface ChangeInfo {
isApplied: boolean
isCommitted?: boolean
}
export type Change = LayoutChange | ChordChange | SettingChange
export const changes = persistentWritable<Change[]>("changes", [])
export interface Overlay {
layout: [Map<number, number>, Map<number, number>, Map<number, number>]
chords: Map<string, Chord & {deleted: boolean}>
settings: Map<number, number>
}
export const overlay = derived(changes, changes => {
const overlay: Overlay = {
layout: [new Map(), new Map(), new Map()],
chords: new Map(),
settings: new Map(),
}
for (const change of changes) {
switch (change.type) {
case ChangeType.Layout:
overlay.layout[change.layer].set(change.id, change.action)
break
case ChangeType.Chord:
overlay.chords.set(JSON.stringify(change.id), {
actions: change.actions,
phrase: change.phrase,
deleted: change.deleted ?? false,
})
break
case ChangeType.Setting:
overlay.settings.set(change.id, change.setting)
break
}
}
return overlay
})
export const settings = derived([overlay, deviceSettings], ([overlay, settings]) =>
settings.map<{value: number} & ChangeInfo>((value, id) => ({
value: overlay.settings.get(id) ?? value,
isApplied: !overlay.settings.has(id),
})),
)
export type KeyInfo = {action: number} & ChangeInfo
export const layout = derived([overlay, deviceLayout], ([overlay, layout]) =>
layout.map(
(actions, layer) =>
actions.map<KeyInfo>((action, id) => ({
action: overlay.layout[layer].get(id) ?? action,
isApplied: !overlay.layout[layer].has(id),
})) as [KeyInfo, KeyInfo, KeyInfo],
),
)
export type ChordInfo = Chord &
ChangeInfo & {phraseChanged: boolean; actionsChanged: boolean; sortBy: string} & {
id: number[]
deleted: boolean
}
export const chords = derived([overlay, deviceChords], ([overlay, chords]) => {
const newChords = new Set(overlay.chords.keys())
const changedChords = chords.map<ChordInfo>(chord => {
const id = JSON.stringify(chord.actions)
if (overlay.chords.has(id)) {
newChords.delete(id)
const changedChord = overlay.chords.get(id)!
return {
id: chord.actions,
// use the old phrase for stable editing
sortBy: chord.phrase.map(it => KEYMAP_CODES[it]?.id ?? it).join(),
actions: changedChord.actions,
phrase: changedChord.phrase,
actionsChanged: id !== JSON.stringify(changedChord.actions),
phraseChanged: JSON.stringify(chord.phrase) !== JSON.stringify(changedChord.phrase),
isApplied: false,
deleted: changedChord.deleted,
}
} else {
return {
id: chord.actions,
sortBy: chord.phrase.map(it => KEYMAP_CODES[it]?.id ?? it).join(),
actions: chord.actions,
phrase: chord.phrase,
phraseChanged: false,
actionsChanged: false,
isApplied: true,
deleted: false,
}
}
})
for (const id of newChords) {
const chord = overlay.chords.get(id)!
changedChords.push({
sortBy: "",
isApplied: false,
actionsChanged: true,
phraseChanged: false,
deleted: chord.deleted,
id: JSON.parse(id),
phrase: chord.phrase,
actions: chord.actions,
})
}
return changedChords.sort(({sortBy: a}, {sortBy: b}) => a.localeCompare(b))
})

1
src/lib/versioning.ts Normal file
View File

@@ -0,0 +1 @@
// TODO

View File

@@ -3,29 +3,36 @@
import "$lib/fonts/material-symbols-rounded.scss"
import "$lib/style/scrollbar.scss"
import "$lib/style/tippy.scss"
import "$lib/style/toggle.scss"
import {onMount} from "svelte"
import "$lib/style/theme.scss"
import {onDestroy, onMount} from "svelte"
import {applyTheme, argbFromHex, themeFromSourceColor} from "@material/material-color-utilities"
import Navigation from "./Navigation.svelte"
import {canAutoConnect} from "$lib/serial/device"
import {initSerial} from "$lib/serial/connection"
import type {LayoutServerData} from "./$types"
import type {LayoutData} from "./$types"
import {browser} from "$app/environment"
import BrowserWarning from "./BrowserWarning.svelte"
import "tippy.js/animations/shift-away.css"
import "tippy.js/dist/tippy.css"
import tippy from "tippy.js"
import {theme, userPreferences} from "$lib/preferences.js"
import {setLocale} from "../i18n/i18n-svelte"
import {LL, setLocale} from "../i18n/i18n-svelte"
import {loadLocale} from "../i18n/i18n-util.sync"
import {detectLocale} from "../i18n/i18n-util"
import type {Locales} from "../i18n/i18n-types"
import Footer from "./Footer.svelte"
import {runLayoutDetection} from "$lib/os-layout.js"
import PageTransition from "./PageTransition.svelte"
import {restoreFromFile} from "$lib/backup/backup"
import {goto} from "$app/navigation"
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
loadLocale(locale)
setLocale(locale)
let stopLayoutDetection: () => void
if (browser) {
stopLayoutDetection = runLayoutDetection()
tippy.setDefaultProps({
animation: "shift-away",
theme: "surface-variant",
@@ -36,7 +43,7 @@
})
}
export let data: LayoutServerData
export let data: LayoutData
onMount(async () => {
theme.subscribe(it => {
@@ -46,10 +53,22 @@
})
if (import.meta.env.TAURI_FAMILY === undefined) {
const {initPwa} = await import("./pwa-setup")
await initPwa()
webManifestLink = await initPwa()
}
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) await initSerial()
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
await initSerial()
}
if (data.importFile) {
restoreFromFile(data.importFile)
const url = new URL(location.href)
url.searchParams.delete("import")
await goto(url.href, {replaceState: true})
}
})
onDestroy(() => {
stopLayoutDetection?.()
})
let webManifestLink = ""
@@ -57,50 +76,26 @@
<svelte:head>
{@html webManifestLink}
<title>amaCC1ng</title>
<meta name="description" content="Tool for CharaChorder devices" />
<title>{$LL.TITLE()}</title>
<meta name="description" content={$LL.DESCRIPTION()} />
<meta name="theme-color" content={data.themeColor} />
</svelte:head>
<Navigation />
<main>
<!-- <PickChangesDialog /> -->
<PageTransition>
<slot />
</main>
</PageTransition>
<Footer />
{#if import.meta.env.TAURI_FAMILY === undefined && browser && !("serial" in navigator)}
<BrowserWarning />
{/if}
<style lang="scss" global>
* {
box-sizing: border-box;
appearance: none;
}
a {
color: var(--md-sys-color-tertiary);
}
label:has(input):hover,
.button:hover:not(:active),
a:hover:not(:active),
button:hover:not(:active) {
filter: brightness(70%);
transition: filter 250ms ease;
&:has(:checked),
&.active {
filter: brightness(120%);
}
&:disabled,
&.disabled {
opacity: 0.5;
filter: none;
}
}
body {
overflow: hidden;
display: flex;
@@ -122,8 +117,7 @@
flex-direction: column;
flex-grow: 1;
align-items: center;
padding: 16px;
padding-inline: 16px;
}
h1 {

View File

@@ -1,2 +1,14 @@
import type {LayoutLoad} from "./$types"
import {browser} from "$app/environment"
import {charaFileFromUriComponent} from "$lib/share/share-url"
export const prerender = true
export const trailingSlash = "always"
export const load = (async ({url, data, fetch}) => {
const importFile = browser && new URLSearchParams(url.search).get("import")
return {
...data,
importFile: importFile ? await charaFileFromUriComponent(importFile, fetch) : undefined,
}
}) satisfies LayoutLoad

View File

@@ -1,12 +0,0 @@
<script>
</script>
<svelte:head>
<title>dot i/o</title>
</svelte:head>
<h1>dot i/o V2</h1>
<section>
<h2>Layout</h2>
</section>

View File

@@ -1,45 +1,14 @@
<script lang="ts">
import {parseCompressed, stringifyCompressed} from "$lib/serial/serialization"
import {chords, layout} from "$lib/serial/connection"
import {preference} from "$lib/preferences"
import type {Chord} from "$lib/serial/chord"
import type {CharaLayout} from "$lib/serialization/layout"
import LL from "../i18n/i18n-svelte"
interface Backup {
isCharaBackup: string
chords: Chord[]
layout: CharaLayout
}
async function downloadBackup() {
const downloadUrl = URL.createObjectURL(
await stringifyCompressed({
isCharaBackup: "v1.0",
chords: $chords,
layout: $layout,
}),
)
const element = document.createElement("a")
element.setAttribute("download", "chords.chb")
element.href = downloadUrl
element.setAttribute("target", "_blank")
element.click()
URL.revokeObjectURL(downloadUrl)
}
async function restoreBackup(event: Event) {
const input = (event.target as HTMLInputElement).files![0]
if (!input) return
const backup = await parseCompressed<Backup>(input)
if (backup.isCharaBackup !== "v1.0") throw new Error("Invalid Backup")
if (backup.chords) {
$chords = backup.chords
}
if (backup.layout) {
$layout = backup.layout
}
}
import {
createChordBackup,
createLayoutBackup,
createSettingsBackup,
downloadBackup,
downloadFile,
restoreBackup,
} from "$lib/backup/backup"
</script>
<section>
@@ -47,9 +16,24 @@
<p class="disclaimer">
<i>{$LL.backup.DISCLAIMER()}</i>
</p>
<fieldset>
<legend>{$LL.backup.INDIVIDUAL()}</legend>
<button on:click={() => downloadFile(createChordBackup())}>
<span class="icon">piano</span>
{$LL.configure.chords.TITLE()}
</button>
<button on:click={() => downloadFile(createLayoutBackup())}>
<span class="icon">keyboard</span>
{$LL.configure.layout.TITLE()}
</button>
<button on:click={() => downloadFile(createSettingsBackup())}>
<span class="icon">settings</span>
{$LL.configure.settings.TITLE()}
</button>
</fieldset>
<div class="save">
<button class="primary" on:click={downloadBackup}
><span class="icon">save</span>{$LL.backup.DOWNLOAD()}</button
><span class="icon">download</span>{$LL.backup.DOWNLOAD()}</button
>
<label class="button"
><input on:input={restoreBackup} type="file" /><span class="icon">settings_backup_restore</span
@@ -72,6 +56,13 @@
}
}
fieldset {
display: flex;
margin-block: 16px;
border: 1px solid currentcolor;
border-radius: 16px;
}
section {
display: flex;
flex-direction: column;
@@ -95,34 +86,4 @@
display: flex;
gap: 4px;
}
.button,
button {
cursor: pointer;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
width: max-content;
height: 48px;
padding-block: 8px;
padding-inline: 16px;
font-family: "Noto Sans Mono", monospace;
font-weight: 600;
color: var(--md-sys-color-on-background);
background: transparent;
border: none;
border-radius: 32px;
transition: all 250ms ease;
}
button.primary {
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
}
</style>

View File

@@ -17,9 +17,7 @@
>{$LL.browserWarning.INFO_BROWSER_SUFFIX()}
</p>
<div>
<a href="https://github.com/Theaninova/dotio/releases" target="_blank"
>{$LL.browserWarning.DOWNLOAD_APP()}</a
>
<p>{$LL.browserWarning.DOWNLOAD_APP()}</p>
</div>
</dialog>
@@ -50,24 +48,14 @@
a {
color: var(--md-sys-color-on-error);
text-decoration: underline;
}
div > a {
div > p {
display: flex;
gap: 8px;
align-items: center;
list-style: none;
&::before {
content: "";
display: inline-block;
width: 24px;
height: 24px;
background: var(--md-sys-color-on-error);
}
}
dialog::backdrop {

Some files were not shown because too many files have changed in this diff Show More