mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-27 04:12:41 +00:00
feat: new sharing system
feat: support legacy layout import
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: 'publish'
|
||||
name: "publish"
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- "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
|
||||
|
||||
@@ -6,6 +6,7 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/src-tauri/target
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
|
||||
}
|
||||
6
.prettierrc.cjs
Normal file
6
.prettierrc.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
...require("@theaninova/prettier-config"),
|
||||
plugins: ["prettier-plugin-svelte"],
|
||||
pluginSearchDirs: ["."],
|
||||
overrides: [{files: "*.svelte", options: {parser: "svelte"}}],
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.0/schema/typesafe-i18n.json",
|
||||
"baseLocale": "en",
|
||||
"adapter": "svelte"
|
||||
}
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||
"baseLocale": "en",
|
||||
"adapter": "svelte"
|
||||
}
|
||||
|
||||
37
CONTRIBUTING.md
Normal file
37
CONTRIBUTING.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Contributing
|
||||
|
||||
## UX Principles
|
||||
|
||||
- **Opinionated.** There should never be two ways to do the same thing.
|
||||
- **Intuitive.** If a feature needs a description to explain it,
|
||||
the feature has failed.
|
||||
- **Simple.** No useless buttons that always need to be pressed.
|
||||
|
||||
## UI Design
|
||||
|
||||
The UI design is based on Material 3.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Nix
|
||||
|
||||
[Enable flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes), then start the development shell using
|
||||
|
||||
```shell
|
||||
nix develop
|
||||
```
|
||||
|
||||
You may need to run through some additional setup to get Rust running inside IntelliJ.
|
||||
|
||||
### Other platforms
|
||||
|
||||
- NodeJS >=18.16
|
||||
- Python >=3.10
|
||||
- Rust Stable (For Tauri Development)
|
||||
|
||||
I know, python in JS projects is extremely annoying, unfortunately,
|
||||
it seems to be the only platform that offers a functional
|
||||
way to subset variable woff2 fonts with ligatures.
|
||||
|
||||
In other words, either have python as a development dependency or
|
||||
serve a 3.5MB icons font of which 99.5% is completely unused.
|
||||
25
README.md
25
README.md
@@ -11,31 +11,6 @@ Get the latest desktop release [here](https://github.com/Theaninova/dotio/releas
|
||||
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.
|
||||
|
||||
## Deployment
|
||||
|
||||
### SSH Setup
|
||||
|
||||
@@ -59,6 +59,7 @@ const config: IconsConfig = {
|
||||
"translate",
|
||||
"play_arrow",
|
||||
"extension",
|
||||
"upload_file",
|
||||
],
|
||||
codePoints: {
|
||||
speed: "e9e4",
|
||||
@@ -70,6 +71,8 @@ const config: IconsConfig = {
|
||||
counter_3: "f782",
|
||||
ios_share: "e6b8",
|
||||
light_mode: "e518",
|
||||
upload_file: "e9fc",
|
||||
no_sound: "e710",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
1273
package-lock.json
generated
1273
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -22,54 +22,53 @@
|
||||
},
|
||||
"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.11",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.12",
|
||||
"@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",
|
||||
"@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",
|
||||
"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.16.5",
|
||||
"vitest": "^0.34.4"
|
||||
},
|
||||
"type": "module",
|
||||
"prettier": "@theaninova/prettier-config"
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -85,7 +85,7 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -9,6 +9,10 @@ const de = {
|
||||
DOWNLOAD: "Kopie Speichern",
|
||||
RESTORE: "Wiederherstellen",
|
||||
},
|
||||
share: {
|
||||
URL_COPIED: "Teilbare URL kopiert!",
|
||||
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
||||
},
|
||||
profile: {
|
||||
TITLE: "Profil",
|
||||
LANGUAGE: "Sprache",
|
||||
|
||||
@@ -8,6 +8,10 @@ const en = {
|
||||
DOWNLOAD: "Download Backup",
|
||||
RESTORE: "Restore",
|
||||
},
|
||||
share: {
|
||||
URL_COPIED: "Sharable URL copied!",
|
||||
EXTRA_DOWNLOAD: "Download as file",
|
||||
},
|
||||
profile: {
|
||||
TITLE: "Profile",
|
||||
LANGUAGE: "Language",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
24
src/lib/compat/legacy-layout-converted.sample.json
Normal file
24
src/lib/compat/legacy-layout-converted.sample.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"charaVersion": 1,
|
||||
"type": "layout",
|
||||
"device": "one",
|
||||
"layout": [
|
||||
[
|
||||
309, 304, 312, 303, 306, 290, 282, 301, 266, 285, 289, 270, 281, 272, 262, 288, 277, 298, 307, 264, 287,
|
||||
268, 332, 311, 274, 286, 308, 329, 310, 280, 358, 512, 515, 513, 514, 313, 319, 318, 321, 320, 326, 315,
|
||||
314, 317, 316, 312, 330, 331, 333, 334, 291, 261, 283, 536, 276, 292, 265, 275, 267, 263, 293, 260, 296,
|
||||
544, 279, 294, 271, 299, 269, 273, 295, 284, 297, 302, 278, 357, 516, 519, 517, 518, 327, 336, 338, 335,
|
||||
337, 328, 325, 322, 323, 324
|
||||
],
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
]
|
||||
}
|
||||
270
src/lib/compat/legacy-layout.sample.csv
Normal file
270
src/lib/compat/legacy-layout.sample.csv
Normal file
@@ -0,0 +1,270 @@
|
||||
A1,0,309
|
||||
A1,1,304
|
||||
A1,2,312
|
||||
A1,3,303
|
||||
A1,4,306
|
||||
A1,5,290
|
||||
A1,6,282
|
||||
A1,7,301
|
||||
A1,8,266
|
||||
A1,9,285
|
||||
A1,10,289
|
||||
A1,11,270
|
||||
A1,12,281
|
||||
A1,13,272
|
||||
A1,14,262
|
||||
A1,15,288
|
||||
A1,16,277
|
||||
A1,17,298
|
||||
A1,18,307
|
||||
A1,19,264
|
||||
A1,20,287
|
||||
A1,21,268
|
||||
A1,22,332
|
||||
A1,23,311
|
||||
A1,24,274
|
||||
A1,25,286
|
||||
A1,26,308
|
||||
A1,27,329
|
||||
A1,28,310
|
||||
A1,29,280
|
||||
A1,30,358
|
||||
A1,31,512
|
||||
A1,32,515
|
||||
A1,33,513
|
||||
A1,34,514
|
||||
A1,35,313
|
||||
A1,36,319
|
||||
A1,37,318
|
||||
A1,38,321
|
||||
A1,39,320
|
||||
A1,40,326
|
||||
A1,41,315
|
||||
A1,42,314
|
||||
A1,43,317
|
||||
A1,44,316
|
||||
A1,45,312
|
||||
A1,46,330
|
||||
A1,47,331
|
||||
A1,48,333
|
||||
A1,49,334
|
||||
A1,50,291
|
||||
A1,51,261
|
||||
A1,52,283
|
||||
A1,53,536
|
||||
A1,54,276
|
||||
A1,55,292
|
||||
A1,56,265
|
||||
A1,57,275
|
||||
A1,58,267
|
||||
A1,59,263
|
||||
A1,60,293
|
||||
A1,61,260
|
||||
A1,62,296
|
||||
A1,63,544
|
||||
A1,64,279
|
||||
A1,65,294
|
||||
A1,66,271
|
||||
A1,67,299
|
||||
A1,68,269
|
||||
A1,69,273
|
||||
A1,70,295
|
||||
A1,71,284
|
||||
A1,72,297
|
||||
A1,73,302
|
||||
A1,74,278
|
||||
A1,75,357
|
||||
A1,76,516
|
||||
A1,77,519
|
||||
A1,78,517
|
||||
A1,79,518
|
||||
A1,80,327
|
||||
A1,81,336
|
||||
A1,82,338
|
||||
A1,83,335
|
||||
A1,84,337
|
||||
A1,85,328
|
||||
A1,86,325
|
||||
A1,87,322
|
||||
A1,88,323
|
||||
A1,89,324
|
||||
A2,0,0
|
||||
A2,1,0
|
||||
A2,2,0
|
||||
A2,3,0
|
||||
A2,4,0
|
||||
A2,5,0
|
||||
A2,6,0
|
||||
A2,7,0
|
||||
A2,8,0
|
||||
A2,9,0
|
||||
A2,10,0
|
||||
A2,11,0
|
||||
A2,12,0
|
||||
A2,13,0
|
||||
A2,14,0
|
||||
A2,15,0
|
||||
A2,16,0
|
||||
A2,17,0
|
||||
A2,18,0
|
||||
A2,19,0
|
||||
A2,20,0
|
||||
A2,21,0
|
||||
A2,22,0
|
||||
A2,23,0
|
||||
A2,24,0
|
||||
A2,25,0
|
||||
A2,26,0
|
||||
A2,27,0
|
||||
A2,28,0
|
||||
A2,29,0
|
||||
A2,30,0
|
||||
A2,31,0
|
||||
A2,32,0
|
||||
A2,33,0
|
||||
A2,34,0
|
||||
A2,35,0
|
||||
A2,36,0
|
||||
A2,37,0
|
||||
A2,38,0
|
||||
A2,39,0
|
||||
A2,40,0
|
||||
A2,41,0
|
||||
A2,42,0
|
||||
A2,43,0
|
||||
A2,44,0
|
||||
A2,45,0
|
||||
A2,46,0
|
||||
A2,47,0
|
||||
A2,48,0
|
||||
A2,49,0
|
||||
A2,50,0
|
||||
A2,51,0
|
||||
A2,52,0
|
||||
A2,53,0
|
||||
A2,54,0
|
||||
A2,55,0
|
||||
A2,56,0
|
||||
A2,57,0
|
||||
A2,58,0
|
||||
A2,59,0
|
||||
A2,60,0
|
||||
A2,61,0
|
||||
A2,62,0
|
||||
A2,63,0
|
||||
A2,64,0
|
||||
A2,65,0
|
||||
A2,66,0
|
||||
A2,67,0
|
||||
A2,68,0
|
||||
A2,69,0
|
||||
A2,70,0
|
||||
A2,71,0
|
||||
A2,72,0
|
||||
A2,73,0
|
||||
A2,74,0
|
||||
A2,75,0
|
||||
A2,76,0
|
||||
A2,77,0
|
||||
A2,78,0
|
||||
A2,79,0
|
||||
A2,80,0
|
||||
A2,81,0
|
||||
A2,82,0
|
||||
A2,83,0
|
||||
A2,84,0
|
||||
A2,85,0
|
||||
A2,86,0
|
||||
A2,87,0
|
||||
A2,88,0
|
||||
A2,89,0
|
||||
A3,0,0
|
||||
A3,1,0
|
||||
A3,2,0
|
||||
A3,3,0
|
||||
A3,4,0
|
||||
A3,5,0
|
||||
A3,6,0
|
||||
A3,7,0
|
||||
A3,8,0
|
||||
A3,9,0
|
||||
A3,10,0
|
||||
A3,11,0
|
||||
A3,12,0
|
||||
A3,13,0
|
||||
A3,14,0
|
||||
A3,15,0
|
||||
A3,16,0
|
||||
A3,17,0
|
||||
A3,18,0
|
||||
A3,19,0
|
||||
A3,20,0
|
||||
A3,21,0
|
||||
A3,22,0
|
||||
A3,23,0
|
||||
A3,24,0
|
||||
A3,25,0
|
||||
A3,26,0
|
||||
A3,27,0
|
||||
A3,28,0
|
||||
A3,29,0
|
||||
A3,30,0
|
||||
A3,31,0
|
||||
A3,32,0
|
||||
A3,33,0
|
||||
A3,34,0
|
||||
A3,35,0
|
||||
A3,36,0
|
||||
A3,37,0
|
||||
A3,38,0
|
||||
A3,39,0
|
||||
A3,40,0
|
||||
A3,41,0
|
||||
A3,42,0
|
||||
A3,43,0
|
||||
A3,44,0
|
||||
A3,45,0
|
||||
A3,46,0
|
||||
A3,47,0
|
||||
A3,48,0
|
||||
A3,49,0
|
||||
A3,50,0
|
||||
A3,51,0
|
||||
A3,52,0
|
||||
A3,53,0
|
||||
A3,54,0
|
||||
A3,55,0
|
||||
A3,56,0
|
||||
A3,57,0
|
||||
A3,58,0
|
||||
A3,59,0
|
||||
A3,60,0
|
||||
A3,61,0
|
||||
A3,62,0
|
||||
A3,63,0
|
||||
A3,64,0
|
||||
A3,65,0
|
||||
A3,66,0
|
||||
A3,67,0
|
||||
A3,68,0
|
||||
A3,69,0
|
||||
A3,70,0
|
||||
A3,71,0
|
||||
A3,72,0
|
||||
A3,73,0
|
||||
A3,74,0
|
||||
A3,75,0
|
||||
A3,76,0
|
||||
A3,77,0
|
||||
A3,78,0
|
||||
A3,79,0
|
||||
A3,80,0
|
||||
A3,81,0
|
||||
A3,82,0
|
||||
A3,83,0
|
||||
A3,84,0
|
||||
A3,85,0
|
||||
A3,86,0
|
||||
A3,87,0
|
||||
A3,88,0
|
||||
A3,89,0
|
||||
|
18
src/lib/compat/legacy-layout.spec.ts
Normal file
18
src/lib/compat/legacy-layout.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {describe, expect, it} from "vitest"
|
||||
import legacyLayout from "./legacy-layout.sample.csv?raw"
|
||||
import legacyLayoutConverted from "./legacy-layout-converted.sample.json"
|
||||
import {csvLayoutToJson, isCsvLayout} from "./legacy-layout"
|
||||
|
||||
describe("legacy layout", () => {
|
||||
it("should detect a legacy layout", () => {
|
||||
expect(isCsvLayout(legacyLayout)).to.be.true
|
||||
})
|
||||
|
||||
it("should not detect chord maps as layouts", () => {
|
||||
expect(isCsvLayout("e + h + t,the")).to.be.false
|
||||
})
|
||||
|
||||
it("should convert legacy layouts", () => {
|
||||
expect(csvLayoutToJson(legacyLayout)).to.deep.equal(legacyLayoutConverted)
|
||||
})
|
||||
})
|
||||
25
src/lib/compat/legacy-layout.ts
Normal file
25
src/lib/compat/legacy-layout.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type {CharaLayoutFile} from "$lib/share/chara-file"
|
||||
|
||||
/**
|
||||
* Converts a legacy CSV-based layout to the modern JSON-based format
|
||||
*/
|
||||
export function csvLayoutToJson(csv: string, device: CharaLayoutFile["device"] = "one"): CharaLayoutFile {
|
||||
const layout: CharaLayoutFile = {
|
||||
charaVersion: 1,
|
||||
type: "layout",
|
||||
device,
|
||||
layout: [[], [], []],
|
||||
}
|
||||
|
||||
for (const layer of csv.split("\n")) {
|
||||
const [layerId, key, action] = layer.substring(1).split(",").map(Number)
|
||||
|
||||
layout.layout[Number(layerId) - 1][Number(key)] = Number(action)
|
||||
}
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
export function isCsvLayout(csv: string): boolean {
|
||||
return /^(A[123],\d+,\d+\n?)+$/.test(csv)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||
|
||||
export let exact: number | undefined = undefined
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
results = query ? index.search(searchInput.value) : defaultActions
|
||||
}
|
||||
|
||||
let customValue = undefined
|
||||
let customValue: number | undefined = undefined
|
||||
const defaultActions: string[] = [
|
||||
charaActions,
|
||||
mouseActions,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function toBase64(blob: Blob): Promise<string> {
|
||||
.replace(/^data:application\/octet-stream;base64,/, "")
|
||||
.replaceAll("+", ".")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "-")}-`,
|
||||
.replaceAll("=", "-")}`,
|
||||
)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
@@ -23,7 +23,6 @@ export async function toBase64(blob: Blob): Promise<string> {
|
||||
export async function fromBase64(base64: string): Promise<Blob> {
|
||||
return fetch(
|
||||
`data:application/octet-stream;base64,${base64
|
||||
.replace(/-$/, "")
|
||||
.replaceAll(".", "+")
|
||||
.replaceAll("_", "/")
|
||||
.replaceAll("-", "=")}`,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import {compressActions, decompressActions} from "./actions"
|
||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
||||
|
||||
export interface NewCharaLayout {
|
||||
charaLayoutVersion: 1
|
||||
device: "one" | "lite" | string
|
||||
/**
|
||||
* Layers A1-A3, with numeric action codes on each
|
||||
*/
|
||||
layers: [number[], number[], number[]]
|
||||
}
|
||||
|
||||
export type CharaLayout = [number[], number[], number[]]
|
||||
|
||||
/**
|
||||
|
||||
52
src/lib/share/action-array.ts
Normal file
52
src/lib/share/action-array.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {compressActions, decompressActions} from "$lib/serialization/actions"
|
||||
import {CHARA_FILE_TYPES} from "$lib/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"))
|
||||
return concatUint8Arrays(out, compressActions(array as number[]))
|
||||
} 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): ActionArray {
|
||||
const reader = new DataView(raw.buffer)
|
||||
const length = reader.getUint32(0)
|
||||
const type = CHARA_FILE_TYPES[reader.getUint8(4)]
|
||||
|
||||
if (type === "number") {
|
||||
return decompressActions(raw.slice(5, 5 + length))
|
||||
} else if (type === "array") {
|
||||
const innerLength = reader.getUint32(5)
|
||||
const out = []
|
||||
let cursor = 5
|
||||
for (let i = 0; i < length; i++) {
|
||||
out.push(deserializeActionArray(raw.slice(cursor, cursor + innerLength)))
|
||||
cursor += innerLength
|
||||
}
|
||||
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
|
||||
}
|
||||
15
src/lib/share/chara-file.ts
Normal file
15
src/lib/share/chara-file.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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 type CharaFiles = CharaLayoutFile | CharaChordFile
|
||||
61
src/lib/share/share-url.ts
Normal file
61
src/lib/share/share-url.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type {CharaFile, CharaFiles} from "$lib/share/chara-file"
|
||||
import type {ActionArray} from "$lib/share/action-array"
|
||||
import {deserializeActionArray, serializeActionArray} from "$lib/share/action-array"
|
||||
import {fromBase64, toBase64} from "$lib/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"]],
|
||||
}
|
||||
|
||||
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): Promise<T> {
|
||||
const [fileType, version, ...values] = uriComponent.split(sep)
|
||||
const file: any = {type: fileType, version: Number(version)}
|
||||
|
||||
for (const [key, type] of keys[fileType as keyof typeof keys]) {
|
||||
const value = values.pop()!
|
||||
if (type === "string") {
|
||||
file[key] = value
|
||||
} else if (type === "array") {
|
||||
const stream = (await fromBase64(value)).stream().pipeThrough(new DecompressionStream("deflate"))
|
||||
const actions = new Uint8Array(await new Response(stream).arrayBuffer())
|
||||
file[key] = deserializeActionArray(actions)
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
@@ -57,17 +57,6 @@
|
||||
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 {
|
||||
|
||||
6
src/routes/config/SharePopup.svelte
Normal file
6
src/routes/config/SharePopup.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import LL from "../../i18n/i18n-svelte"
|
||||
</script>
|
||||
|
||||
<h4>{$LL.share.URL_COPIED()}</h4>
|
||||
<button>{$LL.share.EXTRA_DOWNLOAD()}</button>
|
||||
@@ -3,33 +3,70 @@
|
||||
import {layout} from "$lib/serial/connection"
|
||||
import tippy from "tippy.js"
|
||||
import {onMount} from "svelte"
|
||||
import {layoutAsUrlComponent, layoutFromUrlComponent} from "$lib/serialization/layout"
|
||||
import Layout from "$lib/components/layout/Layout.svelte"
|
||||
import {csvLayoutToJson, isCsvLayout} from "$lib/compat/legacy-layout"
|
||||
import {charaFileFromUriComponent, charaFileToUriComponent} from "$lib/share/share-url"
|
||||
import type {CharaLayoutFile} from "$lib/share/chara-file"
|
||||
import SharePopup from "../SharePopup.svelte"
|
||||
|
||||
onMount(async () => {
|
||||
const url = new URL(window.location.href)
|
||||
if (url.searchParams.has("layout")) {
|
||||
$layout = await layoutFromUrlComponent(url.searchParams.get("layout")!)
|
||||
if (url.searchParams.has("import")) {
|
||||
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
|
||||
if (file.type === "layout") {
|
||||
$layout = file.layout
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function shareLayout(event: Event) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set("layout", await layoutAsUrlComponent($layout))
|
||||
url.searchParams.set(
|
||||
"import",
|
||||
await charaFileToUriComponent({
|
||||
charaVersion: 1,
|
||||
type: "layout",
|
||||
device: "one",
|
||||
layout: $layout,
|
||||
}),
|
||||
)
|
||||
await navigator.clipboard.writeText(url.toString())
|
||||
let shareComponent: SharePopup
|
||||
tippy(event.target as HTMLElement, {
|
||||
content: "Share url copied!",
|
||||
delay: [0, 1000000],
|
||||
onCreate(instance) {
|
||||
const target = instance.popper.querySelector(".tippy-content")!
|
||||
shareComponent = new SharePopup({target})
|
||||
},
|
||||
onHidden(instance) {
|
||||
instance.destroy()
|
||||
},
|
||||
onDestroy(instance) {
|
||||
shareComponent.$destroy()
|
||||
},
|
||||
}).show()
|
||||
}
|
||||
|
||||
async function importLayout() {
|
||||
const file = await fileInput.files?.item(0)?.text()
|
||||
if (!file) return
|
||||
const importedLayout = isCsvLayout(file) ? csvLayoutToJson(file) : (JSON.parse(file) as CharaLayoutFile)
|
||||
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1) $layout = importedLayout.layout
|
||||
}
|
||||
|
||||
let fileInput: HTMLInputElement
|
||||
</script>
|
||||
|
||||
<svelte:window use:share={shareLayout} />
|
||||
|
||||
<section>
|
||||
<label class="icon"
|
||||
>upload_file<input
|
||||
bind:this={fileInput}
|
||||
on:input={importLayout}
|
||||
type="file"
|
||||
accept="text/csv, application/json"
|
||||
/></label
|
||||
>
|
||||
<Layout />
|
||||
</section>
|
||||
|
||||
@@ -37,4 +74,8 @@
|
||||
section {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>iFrame Sandbox</title>
|
||||
<script>
|
||||
let ongoingRequest
|
||||
let resolveRequest
|
||||
let source
|
||||
async function post(channel, args) {
|
||||
while (ongoingRequest) {
|
||||
await ongoingRequest
|
||||
}
|
||||
ongoingRequest = new Promise(resolve => {
|
||||
resolveRequest = resolve
|
||||
source.postMessage([channel, args], "*")
|
||||
})
|
||||
ongoingRequest.then(() => {
|
||||
ongoingRequest = undefined
|
||||
})
|
||||
return ongoingRequest
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if ("response" in event.data) {
|
||||
resolveRequest(event.data.response)
|
||||
} else {
|
||||
source = event.source
|
||||
|
||||
var Action = event.data.actionCodes
|
||||
Object.assign(Action, Object.fromEntries(Object.values(event.data.actionCodes).filter(it => !!it.id).map(it => [it.id, it])))
|
||||
|
||||
var Chara = {}
|
||||
for (const fn of event.data.charaChannels) {
|
||||
Chara[fn] = (...args) => post(fn, args)
|
||||
<head>
|
||||
<title>iFrame Sandbox</title>
|
||||
<script>
|
||||
let ongoingRequest
|
||||
let resolveRequest
|
||||
let source
|
||||
async function post(channel, args) {
|
||||
while (ongoingRequest) {
|
||||
await ongoingRequest
|
||||
}
|
||||
|
||||
eval(`(async function(){${event.data.script}})()`)
|
||||
ongoingRequest = new Promise(resolve => {
|
||||
resolveRequest = resolve
|
||||
source.postMessage([channel, args], "*")
|
||||
})
|
||||
ongoingRequest.then(() => {
|
||||
ongoingRequest = undefined
|
||||
})
|
||||
return ongoingRequest
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
if ("response" in event.data) {
|
||||
resolveRequest(event.data.response)
|
||||
} else {
|
||||
source = event.source
|
||||
|
||||
var Action = event.data.actionCodes
|
||||
Object.assign(
|
||||
Action,
|
||||
Object.fromEntries(
|
||||
Object.values(event.data.actionCodes)
|
||||
.filter(it => !!it.id)
|
||||
.map(it => [it.id, it]),
|
||||
),
|
||||
)
|
||||
|
||||
var Chara = {}
|
||||
for (const fn of event.data.charaChannels) {
|
||||
Chara[fn] = (...args) => post(fn, args)
|
||||
}
|
||||
|
||||
eval(`(async function(){${event.data.script}})()`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user