mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-27 11:32:05 +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:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
name: 'publish'
|
name: "publish"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
|
||||||
releaseName: 'App v__VERSION__'
|
releaseName: "App v__VERSION__"
|
||||||
releaseBody: 'See the assets to download this version and install.'
|
releaseBody: "See the assets to download this version and install."
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
/src-tauri/target
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
# Ignore files for PNPM, NPM and YARN
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|||||||
@@ -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",
|
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||||
"baseLocale": "en",
|
"baseLocale": "en",
|
||||||
"adapter": "svelte"
|
"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
|
I aim to create a new site that offers an easier, visually pleasing
|
||||||
and more complete way to configure and learn CharaChorder devices.
|
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
|
## Deployment
|
||||||
|
|
||||||
### SSH Setup
|
### SSH Setup
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const config: IconsConfig = {
|
|||||||
"translate",
|
"translate",
|
||||||
"play_arrow",
|
"play_arrow",
|
||||||
"extension",
|
"extension",
|
||||||
|
"upload_file",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
@@ -70,6 +71,8 @@ const config: IconsConfig = {
|
|||||||
counter_3: "f782",
|
counter_3: "f782",
|
||||||
ios_share: "e6b8",
|
ios_share: "e6b8",
|
||||||
light_mode: "e518",
|
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": {
|
"devDependencies": {
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.9.0",
|
||||||
"@codemirror/commands": "^6.2.4",
|
"@codemirror/commands": "^6.2.5",
|
||||||
"@codemirror/lang-javascript": "^6.1.9",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/language": "^6.8.0",
|
"@codemirror/language": "^6.9.0",
|
||||||
"@codemirror/state": "^6.2.1",
|
"@codemirror/state": "^6.2.1",
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.6",
|
"@fontsource-variable/material-symbols-rounded": "^5.0.11",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.7",
|
"@fontsource-variable/noto-sans-mono": "^5.0.12",
|
||||||
"@material/material-color-utilities": "^0.2.7",
|
"@material/material-color-utilities": "^0.2.7",
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
"@sveltejs/adapter-static": "^2.0.3",
|
"@sveltejs/adapter-static": "^2.0.3",
|
||||||
"@sveltejs/kit": "^1.22.4",
|
"@sveltejs/kit": "^1.24.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.3",
|
"@sveltejs/vite-plugin-svelte": "^2.4.5",
|
||||||
"@tauri-apps/api": "^1.4.0",
|
"@tauri-apps/api": "^1.4.0",
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
"@theaninova/prettier-config": "^1.0.0",
|
"@theaninova/prettier-config": "^1.0.0",
|
||||||
"@types/dom-view-transitions": "^1.0.1",
|
"@types/dom-view-transitions": "^1.0.1",
|
||||||
"@types/flexsearch": "^0.7.3",
|
"@types/flexsearch": "^0.7.3",
|
||||||
"@types/w3c-web-serial": "^1.0.3",
|
"@types/w3c-web-serial": "^1.0.3",
|
||||||
"@vite-pwa/sveltekit": "^0.2.5",
|
"@vite-pwa/sveltekit": "^0.2.7",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.15",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"cypress": "^12.17.3",
|
"cypress": "^13.1.0",
|
||||||
"flexsearch": "^0.7.31",
|
"flexsearch": "^0.7.31",
|
||||||
"fontkit": "^2.0.2",
|
"fontkit": "^2.0.2",
|
||||||
"glob": "^10.3.3",
|
"glob": "^10.3.4",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"prettier": "^3.0.1",
|
"prettier": "^3.0.3",
|
||||||
"prettier-plugin-svelte": "^3.0.3",
|
"prettier-plugin-svelte": "^3.0.3",
|
||||||
"sass": "^1.64.2",
|
"sass": "^1.66.1",
|
||||||
"stylelint": "^15.10.2",
|
"stylelint": "^15.10.3",
|
||||||
"stylelint-config-clean-order": "^5.0.1",
|
"stylelint-config-clean-order": "^5.2.0",
|
||||||
"stylelint-config-html": "^1.1.0",
|
"stylelint-config-html": "^1.1.0",
|
||||||
"stylelint-config-prettier-scss": "^1.0.0",
|
"stylelint-config-prettier-scss": "^1.0.0",
|
||||||
"stylelint-config-recommended-scss": "^12.0.0",
|
"stylelint-config-recommended-scss": "^13.0.0",
|
||||||
"stylelint-config-standard-scss": "^10.0.0",
|
"stylelint-config-standard-scss": "^11.0.0",
|
||||||
"svelte": "^4.1.2",
|
"svelte": "^4.2.0",
|
||||||
"svelte-check": "^3.4.6",
|
"svelte-check": "^3.5.1",
|
||||||
"svelte-preprocess": "^5.0.4",
|
"svelte-preprocess": "^5.0.4",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typesafe-i18n": "^5.26.0",
|
"typesafe-i18n": "^5.26.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.8",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-mkcert": "^1.16.0",
|
"vite-plugin-mkcert": "^1.16.0",
|
||||||
"vite-plugin-pwa": "^0.16.4",
|
"vite-plugin-pwa": "^0.16.5",
|
||||||
"vitest": "^0.34.1"
|
"vitest": "^0.34.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module"
|
||||||
"prettier": "@theaninova/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -85,7 +85,7 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ const de = {
|
|||||||
DOWNLOAD: "Kopie Speichern",
|
DOWNLOAD: "Kopie Speichern",
|
||||||
RESTORE: "Wiederherstellen",
|
RESTORE: "Wiederherstellen",
|
||||||
},
|
},
|
||||||
|
share: {
|
||||||
|
URL_COPIED: "Teilbare URL kopiert!",
|
||||||
|
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
TITLE: "Profil",
|
TITLE: "Profil",
|
||||||
LANGUAGE: "Sprache",
|
LANGUAGE: "Sprache",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ const en = {
|
|||||||
DOWNLOAD: "Download Backup",
|
DOWNLOAD: "Download Backup",
|
||||||
RESTORE: "Restore",
|
RESTORE: "Restore",
|
||||||
},
|
},
|
||||||
|
share: {
|
||||||
|
URL_COPIED: "Sharable URL copied!",
|
||||||
|
EXTRA_DOWNLOAD: "Download as file",
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
TITLE: "Profile",
|
TITLE: "Profile",
|
||||||
LANGUAGE: "Language",
|
LANGUAGE: "Language",
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { FormattersInitializer } from 'typesafe-i18n'
|
import type {FormattersInitializer} from "typesafe-i18n"
|
||||||
import type { Locales, Formatters } from './i18n-types'
|
import type {Locales, Formatters} from "./i18n-types"
|
||||||
|
|
||||||
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
||||||
|
const formatters: Formatters = {
|
||||||
|
// add your formatter functions here
|
||||||
|
}
|
||||||
|
|
||||||
const formatters: Formatters = {
|
return formatters
|
||||||
// add your formatter functions here
|
|
||||||
}
|
|
||||||
|
|
||||||
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">
|
<script lang="ts">
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||||
|
|
||||||
export let exact: number | undefined = undefined
|
export let exact: number | undefined = undefined
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {serialLog, serialPort} from "$lib/serial/connection"
|
import {serialLog, serialPort} from "$lib/serial/connection"
|
||||||
import {slide} from "svelte/transition"
|
import {slide} from "svelte/transition"
|
||||||
|
|
||||||
function submit(event: InputEvent) {
|
function submit(event: Event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
$serialPort.send(value.trim())
|
$serialPort.send(value.trim())
|
||||||
value = ""
|
value = ""
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
results = query ? index.search(searchInput.value) : defaultActions
|
results = query ? index.search(searchInput.value) : defaultActions
|
||||||
}
|
}
|
||||||
|
|
||||||
let customValue = undefined
|
let customValue: number | undefined = undefined
|
||||||
const defaultActions: string[] = [
|
const defaultActions: string[] = [
|
||||||
charaActions,
|
charaActions,
|
||||||
mouseActions,
|
mouseActions,
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
font-family: "Material Symbols Rounded";
|
font-family: "Material Symbols Rounded";
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-feature-settings: "liga";
|
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);
|
"GRAD" var(--icon-grade, 0);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function toBase64(blob: Blob): Promise<string> {
|
|||||||
.replace(/^data:application\/octet-stream;base64,/, "")
|
.replace(/^data:application\/octet-stream;base64,/, "")
|
||||||
.replaceAll("+", ".")
|
.replaceAll("+", ".")
|
||||||
.replaceAll("/", "_")
|
.replaceAll("/", "_")
|
||||||
.replaceAll("=", "-")}-`,
|
.replaceAll("=", "-")}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
@@ -23,7 +23,6 @@ export async function toBase64(blob: Blob): Promise<string> {
|
|||||||
export async function fromBase64(base64: string): Promise<Blob> {
|
export async function fromBase64(base64: string): Promise<Blob> {
|
||||||
return fetch(
|
return fetch(
|
||||||
`data:application/octet-stream;base64,${base64
|
`data:application/octet-stream;base64,${base64
|
||||||
.replace(/-$/, "")
|
|
||||||
.replaceAll(".", "+")
|
.replaceAll(".", "+")
|
||||||
.replaceAll("_", "/")
|
.replaceAll("_", "/")
|
||||||
.replaceAll("-", "=")}`,
|
.replaceAll("-", "=")}`,
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import {compressActions, decompressActions} from "./actions"
|
import {compressActions, decompressActions} from "./actions"
|
||||||
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
import {fromBase64, toBase64} from "$lib/serialization/base64"
|
||||||
|
|
||||||
|
export interface NewCharaLayout {
|
||||||
|
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[]]
|
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;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-on-error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog::backdrop {
|
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 {layout} from "$lib/serial/connection"
|
||||||
import tippy from "tippy.js"
|
import tippy from "tippy.js"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {layoutAsUrlComponent, layoutFromUrlComponent} from "$lib/serialization/layout"
|
|
||||||
import Layout from "$lib/components/layout/Layout.svelte"
|
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 () => {
|
onMount(async () => {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
if (url.searchParams.has("layout")) {
|
if (url.searchParams.has("import")) {
|
||||||
$layout = await layoutFromUrlComponent(url.searchParams.get("layout")!)
|
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
|
||||||
|
if (file.type === "layout") {
|
||||||
|
$layout = file.layout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function shareLayout(event: Event) {
|
async function shareLayout(event: Event) {
|
||||||
const url = new URL(window.location.href)
|
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())
|
await navigator.clipboard.writeText(url.toString())
|
||||||
|
let shareComponent: SharePopup
|
||||||
tippy(event.target as HTMLElement, {
|
tippy(event.target as HTMLElement, {
|
||||||
content: "Share url copied!",
|
onCreate(instance) {
|
||||||
delay: [0, 1000000],
|
const target = instance.popper.querySelector(".tippy-content")!
|
||||||
|
shareComponent = new SharePopup({target})
|
||||||
|
},
|
||||||
onHidden(instance) {
|
onHidden(instance) {
|
||||||
instance.destroy()
|
instance.destroy()
|
||||||
},
|
},
|
||||||
|
onDestroy(instance) {
|
||||||
|
shareComponent.$destroy()
|
||||||
|
},
|
||||||
}).show()
|
}).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>
|
</script>
|
||||||
|
|
||||||
<svelte:window use:share={shareLayout} />
|
<svelte:window use:share={shareLayout} />
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
<label class="icon"
|
||||||
|
>upload_file<input
|
||||||
|
bind:this={fileInput}
|
||||||
|
on:input={importLayout}
|
||||||
|
type="file"
|
||||||
|
accept="text/csv, application/json"
|
||||||
|
/></label
|
||||||
|
>
|
||||||
<Layout />
|
<Layout />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -37,4 +74,8 @@
|
|||||||
section {
|
section {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,42 +1,49 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>iFrame Sandbox</title>
|
<title>iFrame Sandbox</title>
|
||||||
<script>
|
<script>
|
||||||
let ongoingRequest
|
let ongoingRequest
|
||||||
let resolveRequest
|
let resolveRequest
|
||||||
let source
|
let source
|
||||||
async function post(channel, args) {
|
async function post(channel, args) {
|
||||||
while (ongoingRequest) {
|
while (ongoingRequest) {
|
||||||
await 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)
|
|
||||||
}
|
}
|
||||||
|
ongoingRequest = new Promise(resolve => {
|
||||||
eval(`(async function(){${event.data.script}})()`)
|
resolveRequest = resolve
|
||||||
|
source.postMessage([channel, args], "*")
|
||||||
|
})
|
||||||
|
ongoingRequest.then(() => {
|
||||||
|
ongoingRequest = undefined
|
||||||
|
})
|
||||||
|
return ongoingRequest
|
||||||
}
|
}
|
||||||
})
|
|
||||||
</script>
|
window.addEventListener("message", event => {
|
||||||
</head>
|
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>
|
</html>
|
||||||
Reference in New Issue
Block a user